From 9ad64ec98790846f2ae829e2836f2677809acded Mon Sep 17 00:00:00 2001 From: upstream-mirror Date: Wed, 23 Sep 2026 17:17:10 +0500 Subject: [PATCH] snapshot of https://github.com/tauri-apps/plugins-workspace.git (v2, 2026-09-23; blobs > 131072B omitted) --- .cargo/audit.toml | 20 + .changes/config.json | 355 ++ .changes/fix-autostart-windows-disable.md | 5 + .changes/fix-deep-link-linux-unregister.md | 5 + .changes/fix-geo-haptics-specta-skip.md | 8 + .../fix-notification-mobile-permissions.md | 11 + .../fix-notification-windows-permission.md | 6 + .changes/global-api-script-reuse-core.md | 58 + .changes/readme.md | 32 + .changes/update-docs.md | 61 + .github/CODEOWNERS | 5 + .github/CONTRIBUTING.md | 73 + .github/ISSUE_TEMPLATE/bug_report.yml | 63 + .github/ISSUE_TEMPLATE/config.yml | 12 + .github/ISSUE_TEMPLATE/docs_report.md | 5 + .github/ISSUE_TEMPLATE/feature_request.yml | 46 + .github/sponsors/crabnebula.svg | 31 + .github/sponsors/rescue.png | Bin 0 -> 8071 bytes .github/sync-to-mirrors.sh | 136 + .github/workflows/audit-javascript.yml | 49 + .github/workflows/audit-rust.yml | 45 + .github/workflows/check-change-files.yml | 47 + .github/workflows/check-generated-files.yml | 175 + .github/workflows/check-license-header.yml | 31 + .../workflows/covector-comment-on-fork.yml | 30 + .github/workflows/covector-status.yml | 25 + .../workflows/covector-version-or-publish.yml | 84 + .github/workflows/fmt.yml | 55 + .github/workflows/integration-tests.yml | 70 + .github/workflows/lint-javascript.yml | 59 + .github/workflows/lint-rust.yml | 183 + .github/workflows/sync.yml | 46 + .github/workflows/test-android.yml | 78 + .github/workflows/test-api-e2e-mobile.yml | 235 + .github/workflows/test-api-e2e.yml | 176 + .github/workflows/test-rust.yml | 311 ++ .gitignore | 62 + .npmrc | 1 + .prettierignore | 27 + .prettierrc | 6 + .scripts/ci/check-change-files.js | 86 + .scripts/ci/check-license-header.js | 130 + .scripts/ci/collect-macos-crash-reports.sh | 50 + .scripts/ci/has-diff.sh | 10 + .taurignore | 2 + .vscode/extensions.json | 8 + Cargo.toml | 58 + LICENSE.spdx | 20 + LICENSE_APACHE-2.0 | 177 + LICENSE_MIT | 21 + README.md | 62 + SECURITY.md | 23 + eslint.config.js | 39 + examples/api/.setup-cross.sh | 8 + examples/api/.taurignore | 3 + examples/api/CHANGELOG.md | 836 ++++ examples/api/README.md | 28 + examples/api/index.html | 17 + examples/api/isolation-dist/index.html | 10 + examples/api/isolation-dist/index.js | 7 + examples/api/package.json | 51 + examples/api/public/tauri_logo.png | Bin 0 -> 2810 bytes examples/api/src-tauri/.gitignore | 10 + examples/api/src-tauri/.taurignore | 1 + examples/api/src-tauri/CHANGELOG.md | 1037 ++++ examples/api/src-tauri/Cargo.toml | 88 + examples/api/src-tauri/Cross.toml | 11 + examples/api/src-tauri/Info.plist | 14 + examples/api/src-tauri/build.rs | 7 + examples/api/src-tauri/capabilities/base.json | 148 + .../api/src-tauri/capabilities/desktop.json | 19 + .../api/src-tauri/capabilities/mobile.json | 24 + .../api/src-tauri/gen/android/.editorconfig | 12 + examples/api/src-tauri/gen/android/.gitignore | 19 + .../api/src-tauri/gen/android/app/.gitignore | 6 + .../gen/android/app/build.gradle.kts | 69 + .../gen/android/app/proguard-rules.pro | 21 + .../android/app/src/main/AndroidManifest.xml | 57 + .../main/java/com/tauri/api/MainActivity.kt | 11 + .../drawable-v24/ic_launcher_foreground.xml | 30 + .../res/drawable/ic_launcher_background.xml | 170 + .../app/src/main/res/layout/activity_main.xml | 18 + .../src/main/res/mipmap-hdpi/ic_launcher.png | Bin 0 -> 3524 bytes .../mipmap-hdpi/ic_launcher_foreground.png | Bin 0 -> 14102 bytes .../res/mipmap-hdpi/ic_launcher_round.png | Bin 0 -> 3524 bytes .../src/main/res/mipmap-mdpi/ic_launcher.png | Bin 0 -> 3377 bytes .../mipmap-mdpi/ic_launcher_foreground.png | Bin 0 -> 9081 bytes .../res/mipmap-mdpi/ic_launcher_round.png | Bin 0 -> 3377 bytes .../src/main/res/mipmap-xhdpi/ic_launcher.png | Bin 0 -> 7971 bytes .../mipmap-xhdpi/ic_launcher_foreground.png | Bin 0 -> 18900 bytes .../res/mipmap-xhdpi/ic_launcher_round.png | Bin 0 -> 7971 bytes .../main/res/mipmap-xxhdpi/ic_launcher.png | Bin 0 -> 12392 bytes .../mipmap-xxhdpi/ic_launcher_foreground.png | Bin 0 -> 29506 bytes .../res/mipmap-xxhdpi/ic_launcher_round.png | Bin 0 -> 12392 bytes .../main/res/mipmap-xxxhdpi/ic_launcher.png | Bin 0 -> 16751 bytes .../mipmap-xxxhdpi/ic_launcher_foreground.png | Bin 0 -> 40510 bytes .../res/mipmap-xxxhdpi/ic_launcher_round.png | Bin 0 -> 16751 bytes .../app/src/main/res/values-night/themes.xml | 6 + .../app/src/main/res/values/colors.xml | 10 + .../app/src/main/res/values/strings.xml | 4 + .../app/src/main/res/values/themes.xml | 6 + .../app/src/main/res/xml/file_paths.xml | 5 + .../src-tauri/gen/android/build.gradle.kts | 22 + .../gen/android/buildSrc/build.gradle.kts | 23 + .../java/com/tauri/api/kotlin/BuildTask.kt | 68 + .../java/com/tauri/api/kotlin/RustPlugin.kt | 85 + .../src-tauri/gen/android/gradle.properties | 24 + .../android/gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 59203 bytes .../gradle/wrapper/gradle-wrapper.properties | 6 + examples/api/src-tauri/gen/android/gradlew | 185 + .../api/src-tauri/gen/android/gradlew.bat | 89 + .../api/src-tauri/gen/android/settings.gradle | 3 + examples/api/src-tauri/gen/apple/.gitignore | 3 + .../AppIcon.appiconset/AppIcon-20x20@1x.png | Bin 0 -> 1036 bytes .../AppIcon.appiconset/AppIcon-20x20@2x-1.png | Bin 0 -> 2216 bytes .../AppIcon.appiconset/AppIcon-20x20@2x.png | Bin 0 -> 2216 bytes .../AppIcon.appiconset/AppIcon-20x20@3x.png | Bin 0 -> 3512 bytes .../AppIcon.appiconset/AppIcon-29x29@1x.png | Bin 0 -> 1568 bytes .../AppIcon.appiconset/AppIcon-29x29@2x-1.png | Bin 0 -> 3394 bytes .../AppIcon.appiconset/AppIcon-29x29@2x.png | Bin 0 -> 3394 bytes .../AppIcon.appiconset/AppIcon-29x29@3x.png | Bin 0 -> 5228 bytes .../AppIcon.appiconset/AppIcon-40x40@1x.png | Bin 0 -> 2216 bytes .../AppIcon.appiconset/AppIcon-40x40@2x-1.png | Bin 0 -> 4794 bytes .../AppIcon.appiconset/AppIcon-40x40@2x.png | Bin 0 -> 4794 bytes .../AppIcon.appiconset/AppIcon-40x40@3x.png | Bin 0 -> 7288 bytes .../AppIcon.appiconset/AppIcon-512@2x.png | Bin 0 -> 121323 bytes .../AppIcon.appiconset/AppIcon-60x60@2x.png | Bin 0 -> 7288 bytes .../AppIcon.appiconset/AppIcon-60x60@3x.png | Bin 0 -> 11091 bytes .../AppIcon.appiconset/AppIcon-76x76@1x.png | Bin 0 -> 4515 bytes .../AppIcon.appiconset/AppIcon-76x76@2x.png | Bin 0 -> 9257 bytes .../AppIcon-83.5x83.5@2x.png | Bin 0 -> 10219 bytes .../AppIcon.appiconset/Contents.json | 116 + .../gen/apple/Assets.xcassets/Contents.json | 6 + .../src-tauri/gen/apple/ExportOptions.plist | 8 + .../gen/apple/LaunchScreen.storyboard | 30 + examples/api/src-tauri/gen/apple/Podfile | 21 + .../gen/apple/Sources/api/bindings/bindings.h | 8 + .../src-tauri/gen/apple/Sources/api/main.mm | 6 + .../gen/apple/api.xcodeproj/project.pbxproj | 469 ++ .../contents.xcworkspacedata | 7 + .../xcshareddata/WorkspaceSettings.xcsettings | 10 + .../xcshareddata/xcschemes/api_iOS.xcscheme | 123 + .../src-tauri/gen/apple/api_iOS/Info.plist | 52 + .../gen/apple/api_iOS/api_iOS.entitlements | 5 + examples/api/src-tauri/gen/apple/project.yml | 89 + examples/api/src-tauri/icons/128x128.png | Bin 0 -> 11059 bytes examples/api/src-tauri/icons/128x128@2x.png | Bin 0 -> 23137 bytes examples/api/src-tauri/icons/32x32.png | Bin 0 -> 2225 bytes examples/api/src-tauri/icons/icon.ico | Bin 0 -> 37710 bytes examples/api/src-tauri/icons/icon.png | Bin 0 -> 49979 bytes examples/api/src-tauri/icons/tray_icon.png | Bin 0 -> 804 bytes .../icons/tray_icon_with_transparency.ico | Bin 0 -> 4414 bytes .../icons/tray_icon_with_transparency.png | Bin 0 -> 1015 bytes examples/api/src-tauri/locales/pt-BR.wxl | 6 + examples/api/src-tauri/src/cmd.rs | 24 + examples/api/src-tauri/src/lib.rs | 223 + examples/api/src-tauri/src/main.rs | 10 + examples/api/src-tauri/src/tray.rs | 128 + examples/api/src-tauri/tauri.conf.json | 113 + examples/api/src/App.svelte | 503 ++ examples/api/src/app.css | 84 + examples/api/src/lib/TitleBar.svelte | 103 + examples/api/src/lib/utils.js | 15 + examples/api/src/main.ts | 14 + examples/api/src/views/Autostart.svelte | 30 + examples/api/src/views/Biometric.svelte | 30 + examples/api/src/views/Cli.svelte | 29 + examples/api/src/views/Clipboard.svelte | 68 + examples/api/src/views/Communication.svelte | 52 + examples/api/src/views/DeepLink.svelte | 67 + examples/api/src/views/Dialog.svelte | 195 + examples/api/src/views/FileSystem.svelte | 272 + examples/api/src/views/Geolocation.svelte | 29 + examples/api/src/views/Haptics.svelte | 46 + examples/api/src/views/Http.svelte | 103 + examples/api/src/views/Nfc.svelte | 126 + examples/api/src/views/Notifications.svelte | 44 + examples/api/src/views/Opener.svelte | 68 + examples/api/src/views/Positioner.svelte | 40 + examples/api/src/views/Scanner.svelte | 163 + examples/api/src/views/Shell.svelte | 100 + examples/api/src/views/Shortcuts.svelte | 72 + examples/api/src/views/Sql.svelte | 68 + examples/api/src/views/Store.svelte | 114 + examples/api/src/views/Stronghold.svelte | 71 + examples/api/src/views/Updater.svelte | 98 + examples/api/src/views/Upload.svelte | 417 ++ examples/api/src/views/WebRTC.svelte | 56 + examples/api/src/views/WebSocket.svelte | 55 + examples/api/src/views/Welcome.svelte | 47 + examples/api/tsconfig.json | 34 + examples/api/unocss.config.js | 100 + examples/api/updater-test/.gitignore | 5 + examples/api/updater-test/README.md | 10 + examples/api/updater-test/updater.json | 11 + examples/api/vite.config.ts | 34 + package.json | 33 + packages/api-e2e/README.md | 251 + packages/api-e2e/package.json | 62 + packages/api-e2e/tauri.e2e.conf.json | 13 + packages/api-e2e/test/helpers/index.ts | 343 ++ packages/api-e2e/test/helpers/server.ts | 155 + packages/api-e2e/test/specs/app.spec.ts | 71 + packages/api-e2e/test/specs/autostart.spec.ts | 58 + packages/api-e2e/test/specs/cli.spec.ts | 33 + .../test/specs/clipboard-manager.spec.ts | 158 + packages/api-e2e/test/specs/deep-link.spec.ts | 92 + packages/api-e2e/test/specs/fs.spec.ts | 381 ++ .../test/specs/global-shortcut.spec.ts | 103 + packages/api-e2e/test/specs/http.spec.ts | 149 + packages/api-e2e/test/specs/log.spec.ts | 188 + .../api-e2e/test/specs/notification.spec.ts | 141 + packages/api-e2e/test/specs/opener.spec.ts | 46 + packages/api-e2e/test/specs/os.spec.ts | 109 + packages/api-e2e/test/specs/plugins.spec.ts | 234 + .../api-e2e/test/specs/positioner.spec.ts | 137 + packages/api-e2e/test/specs/shell.spec.ts | 238 + packages/api-e2e/test/specs/sql.spec.ts | 168 + packages/api-e2e/test/specs/store.spec.ts | 261 + .../api-e2e/test/specs/stronghold.spec.ts | 248 + packages/api-e2e/test/specs/updater.spec.ts | 77 + packages/api-e2e/test/specs/upload.spec.ts | 161 + packages/api-e2e/test/specs/websocket.spec.ts | 191 + .../api-e2e/test/specs/window-state.spec.ts | 130 + packages/api-e2e/test/types/global.d.ts | 18 + packages/api-e2e/tsconfig.json | 20 + packages/api-e2e/wdio.android.conf.ts | 10 + packages/api-e2e/wdio.conf.ts | 306 ++ packages/api-e2e/wdio.ios.conf.ts | 10 + packages/api-e2e/wdio.mobile.ts | 525 ++ plugins/autostart/CHANGELOG.md | 110 + plugins/autostart/Cargo.toml | 34 + plugins/autostart/LICENSE.spdx | 20 + plugins/autostart/LICENSE_APACHE-2.0 | 177 + plugins/autostart/LICENSE_MIT | 21 + plugins/autostart/README.md | 98 + plugins/autostart/SECURITY.md | 23 + plugins/autostart/api-iife.js | 1 + plugins/autostart/banner.png | Bin 0 -> 45290 bytes plugins/autostart/build.rs | 11 + plugins/autostart/guest-js/index.ts | 54 + plugins/autostart/package.json | 29 + .../autogenerated/commands/disable.toml | 13 + .../autogenerated/commands/enable.toml | 13 + .../autogenerated/commands/is_enabled.toml | 13 + .../permissions/autogenerated/reference.md | 104 + plugins/autostart/permissions/default.toml | 15 + .../autostart/permissions/schemas/schema.json | 342 ++ plugins/autostart/rollup.config.js | 7 + plugins/autostart/src/lib.rs | 288 ++ plugins/autostart/tsconfig.json | 7 + plugins/barcode-scanner/CHANGELOG.md | 155 + plugins/barcode-scanner/Cargo.toml | 40 + plugins/barcode-scanner/LICENSE.spdx | 20 + plugins/barcode-scanner/LICENSE_APACHE-2.0 | 177 + plugins/barcode-scanner/LICENSE_MIT | 21 + plugins/barcode-scanner/README.md | 113 + plugins/barcode-scanner/SECURITY.md | 65 + plugins/barcode-scanner/android/.gitignore | 2 + .../barcode-scanner/android/build.gradle.kts | 40 + .../android/consumer-rules.pro | 21 + .../barcode-scanner/android/settings.gradle | 2 + .../java/ExampleInstrumentedTest.kt | 28 + .../android/src/main/AndroidManifest.xml | 9 + .../src/main/java/BarcodeScannerPlugin.kt | 417 ++ .../android/src/main/java/GraphicOverlay.kt | 180 + .../android/src/test/java/ExampleUnitTest.kt | 21 + plugins/barcode-scanner/api-iife.js | 1 + plugins/barcode-scanner/banner.png | Bin 0 -> 49330 bytes plugins/barcode-scanner/build.rs | 25 + .../contributors/crabnebula.svg | 31 + .../barcode-scanner/contributors/impierce.svg | 21 + plugins/barcode-scanner/guest-js/index.ts | 222 + plugins/barcode-scanner/ios/.gitignore | 10 + plugins/barcode-scanner/ios/Package.swift | 36 + plugins/barcode-scanner/ios/README.md | 3 + .../ios/Sources/BarcodeScannerPlugin.swift | 351 ++ .../ios/Sources/CameraView.swift | 57 + .../barcode-scanner/ios/Sources/Utils.swift | 90 + .../ios/Tests/PluginTests/PluginTests.swift | 13 + plugins/barcode-scanner/package.json | 30 + .../autogenerated/commands/cancel.toml | 13 + .../commands/check_permissions.toml | 13 + .../commands/open_app_settings.toml | 13 + .../commands/request_permissions.toml | 13 + .../autogenerated/commands/scan.toml | 13 + .../autogenerated/commands/vibrate.toml | 13 + .../permissions/autogenerated/reference.md | 183 + .../barcode-scanner/permissions/default.toml | 20 + .../permissions/schemas/schema.json | 378 ++ plugins/barcode-scanner/rollup.config.js | 7 + plugins/barcode-scanner/src/error.rs | 30 + plugins/barcode-scanner/src/lib.rs | 58 + plugins/barcode-scanner/src/mobile.rs | 3 + plugins/barcode-scanner/src/models.rs | 3 + plugins/barcode-scanner/tsconfig.json | 7 + plugins/biometric/CHANGELOG.md | 114 + plugins/biometric/Cargo.toml | 32 + plugins/biometric/LICENSE.spdx | 20 + plugins/biometric/LICENSE_APACHE-2.0 | 177 + plugins/biometric/LICENSE_MIT | 21 + plugins/biometric/README.md | 111 + plugins/biometric/SECURITY.md | 23 + plugins/biometric/android/.gitignore | 2 + plugins/biometric/android/build.gradle.kts | 35 + plugins/biometric/android/consumer-rules.pro | 21 + plugins/biometric/android/settings.gradle | 2 + .../java/ExampleInstrumentedTest.kt | 28 + .../android/src/main/AndroidManifest.xml | 9 + .../src/main/java/BiometricActivity.kt | 129 + .../android/src/main/java/BiometricPlugin.kt | 254 + .../src/main/res/layout/auth_activity.xml | 9 + .../android/src/main/res/values/styles.xml | 10 + .../android/src/test/java/ExampleUnitTest.kt | 21 + plugins/biometric/api-iife.js | 1 + plugins/biometric/build.rs | 18 + plugins/biometric/contributors/crabnebula.svg | 31 + plugins/biometric/contributors/impierce.svg | 21 + plugins/biometric/guest-js/index.ts | 117 + plugins/biometric/ios/.gitignore | 10 + plugins/biometric/ios/Package.swift | 34 + plugins/biometric/ios/README.md | 3 + .../ios/Sources/BiometricPlugin.swift | 153 + .../ios/Tests/PluginTests/PluginTests.swift | 12 + plugins/biometric/package.json | 30 + .../autogenerated/commands/authenticate.toml | 13 + .../autogenerated/commands/status.toml | 13 + .../permissions/autogenerated/reference.md | 75 + plugins/biometric/permissions/default.toml | 13 + .../biometric/permissions/schemas/schema.json | 330 ++ plugins/biometric/rollup.config.js | 7 + plugins/biometric/src/error.rs | 31 + plugins/biometric/src/lib.rs | 82 + plugins/biometric/src/models.rs | 52 + plugins/biometric/tsconfig.json | 7 + plugins/cli/CHANGELOG.md | 114 + plugins/cli/Cargo.toml | 39 + plugins/cli/LICENSE.spdx | 20 + plugins/cli/LICENSE_APACHE-2.0 | 177 + plugins/cli/LICENSE_MIT | 21 + plugins/cli/README.md | 105 + plugins/cli/SECURITY.md | 23 + plugins/cli/api-iife.js | 1 + plugins/cli/banner.png | Bin 0 -> 56764 bytes plugins/cli/build.rs | 11 + plugins/cli/guest-js/index.ts | 83 + plugins/cli/package.json | 29 + .../autogenerated/commands/cli_matches.toml | 13 + .../permissions/autogenerated/reference.md | 43 + plugins/cli/permissions/default.toml | 4 + plugins/cli/permissions/schemas/schema.json | 318 ++ plugins/cli/rollup.config.js | 7 + plugins/cli/src/config.rs | 184 + plugins/cli/src/error.rs | 26 + plugins/cli/src/lib.rs | 73 + plugins/cli/src/parser.rs | 295 ++ plugins/cli/src/parser/macros.rs | 47 + plugins/cli/tsconfig.json | 7 + plugins/clipboard-manager/CHANGELOG.md | 152 + plugins/clipboard-manager/Cargo.toml | 45 + plugins/clipboard-manager/LICENSE.spdx | 20 + plugins/clipboard-manager/LICENSE_APACHE-2.0 | 177 + plugins/clipboard-manager/LICENSE_MIT | 21 + plugins/clipboard-manager/README.md | 96 + plugins/clipboard-manager/SECURITY.md | 23 + plugins/clipboard-manager/android/.gitignore | 2 + .../android/build.gradle.kts | 36 + .../android/consumer-rules.pro | 21 + .../clipboard-manager/android/settings.gradle | 2 + .../java/ExampleInstrumentedTest.kt | 28 + .../android/src/main/AndroidManifest.xml | 3 + .../android/src/main/java/ClipboardPlugin.kt | 142 + .../android/src/test/java/ExampleUnitTest.kt | 21 + plugins/clipboard-manager/api-iife.js | 1 + plugins/clipboard-manager/banner.png | Bin 0 -> 69293 bytes plugins/clipboard-manager/build.rs | 25 + plugins/clipboard-manager/guest-js/index.ts | 159 + plugins/clipboard-manager/ios/.gitignore | 10 + plugins/clipboard-manager/ios/Package.swift | 34 + plugins/clipboard-manager/ios/README.md | 3 + .../ios/Sources/ClipboardPlugin.swift | 52 + .../ios/Tests/PluginTests/PluginTests.swift | 12 + plugins/clipboard-manager/package.json | 29 + .../autogenerated/commands/clear.toml | 13 + .../autogenerated/commands/read_image.toml | 13 + .../autogenerated/commands/read_text.toml | 13 + .../autogenerated/commands/write_html.toml | 13 + .../autogenerated/commands/write_image.toml | 13 + .../autogenerated/commands/write_text.toml | 13 + .../permissions/autogenerated/reference.md | 173 + .../permissions/default.toml | 11 + .../permissions/schemas/schema.json | 378 ++ plugins/clipboard-manager/rollup.config.js | 7 + plugins/clipboard-manager/src/commands.rs | 80 + plugins/clipboard-manager/src/desktop.rs | 148 + plugins/clipboard-manager/src/error.rs | 39 + plugins/clipboard-manager/src/lib.rs | 70 + plugins/clipboard-manager/src/mobile.rs | 149 + plugins/clipboard-manager/tsconfig.json | 7 + .../.well-known/apple-app-site-association | 17 + plugins/deep-link/.test-server/server.js | 27 + plugins/deep-link/CHANGELOG.md | 203 + plugins/deep-link/Cargo.toml | 54 + plugins/deep-link/LICENSE.spdx | 20 + plugins/deep-link/LICENSE_APACHE-2.0 | 177 + plugins/deep-link/LICENSE_MIT | 21 + plugins/deep-link/README.md | 192 + plugins/deep-link/SECURITY.md | 23 + plugins/deep-link/android/.gitignore | 2 + plugins/deep-link/android/build.gradle.kts | 35 + plugins/deep-link/android/consumer-rules.pro | 21 + plugins/deep-link/android/settings.gradle | 2 + .../java/ExampleInstrumentedTest.kt | 28 + .../android/src/main/AndroidManifest.xml | 3 + .../android/src/main/java/DeepLinkPlugin.kt | 174 + .../android/src/test/java/ExampleUnitTest.kt | 21 + plugins/deep-link/api-iife.js | 1 + plugins/deep-link/banner.png | Bin 0 -> 41678 bytes plugins/deep-link/build.rs | 188 + plugins/deep-link/contributors/crabnebula.svg | 31 + plugins/deep-link/contributors/impierce.svg | 21 + plugins/deep-link/examples/app/.gitignore | 25 + plugins/deep-link/examples/app/CHANGELOG.md | 213 + plugins/deep-link/examples/app/README.md | 7 + plugins/deep-link/examples/app/index.html | 65 + plugins/deep-link/examples/app/package.json | 21 + .../deep-link/examples/app/public/svelte.svg | 1 + .../deep-link/examples/app/public/tauri.svg | 6 + .../deep-link/examples/app/public/vite.svg | 1 + .../examples/app/src-tauri/.gitignore | 7 + .../examples/app/src-tauri/Cargo.toml | 28 + .../app/src-tauri/apple-app-site-association | 15 + .../app/src-tauri/assetlinks.json.txt | 1 + .../deep-link/examples/app/src-tauri/build.rs | 7 + .../app/src-tauri/capabilities/app.json | 11 + .../app/src-tauri/gen/android/.editorconfig | 12 + .../app/src-tauri/gen/android/.gitignore | 19 + .../app/src-tauri/gen/android/app/.gitignore | 6 + .../gen/android/app/build.gradle.kts | 70 + .../gen/android/app/proguard-rules.pro | 21 + .../android/app/src/main/AndroidManifest.xml | 74 + .../tauri/deep_link_example/MainActivity.kt | 11 + .../drawable-v24/ic_launcher_foreground.xml | 30 + .../res/drawable/ic_launcher_background.xml | 170 + .../app/src/main/res/layout/activity_main.xml | 18 + .../src/main/res/mipmap-hdpi/ic_launcher.png | Bin 0 -> 3524 bytes .../mipmap-hdpi/ic_launcher_foreground.png | Bin 0 -> 14102 bytes .../res/mipmap-hdpi/ic_launcher_round.png | Bin 0 -> 3524 bytes .../src/main/res/mipmap-mdpi/ic_launcher.png | Bin 0 -> 3377 bytes .../mipmap-mdpi/ic_launcher_foreground.png | Bin 0 -> 9081 bytes .../res/mipmap-mdpi/ic_launcher_round.png | Bin 0 -> 3377 bytes .../src/main/res/mipmap-xhdpi/ic_launcher.png | Bin 0 -> 7971 bytes .../mipmap-xhdpi/ic_launcher_foreground.png | Bin 0 -> 18900 bytes .../res/mipmap-xhdpi/ic_launcher_round.png | Bin 0 -> 7971 bytes .../main/res/mipmap-xxhdpi/ic_launcher.png | Bin 0 -> 12392 bytes .../mipmap-xxhdpi/ic_launcher_foreground.png | Bin 0 -> 29506 bytes .../res/mipmap-xxhdpi/ic_launcher_round.png | Bin 0 -> 12392 bytes .../main/res/mipmap-xxxhdpi/ic_launcher.png | Bin 0 -> 16751 bytes .../mipmap-xxxhdpi/ic_launcher_foreground.png | Bin 0 -> 40510 bytes .../res/mipmap-xxxhdpi/ic_launcher_round.png | Bin 0 -> 16751 bytes .../app/src/main/res/values-night/themes.xml | 6 + .../app/src/main/res/values/colors.xml | 10 + .../app/src/main/res/values/strings.xml | 4 + .../app/src/main/res/values/themes.xml | 6 + .../app/src/main/res/xml/file_paths.xml | 5 + .../src-tauri/gen/android/build.gradle.kts | 22 + .../gen/android/buildSrc/build.gradle.kts | 23 + .../deep_link_example/kotlin/BuildTask.kt | 52 + .../deep_link_example/kotlin/RustPlugin.kt | 85 + .../src-tauri/gen/android/gradle.properties | 24 + .../android/gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 59203 bytes .../gradle/wrapper/gradle-wrapper.properties | 6 + .../app/src-tauri/gen/android/gradlew | 185 + .../app/src-tauri/gen/android/gradlew.bat | 89 + .../app/src-tauri/gen/android/settings.gradle | 3 + .../app/src-tauri/gen/apple/.gitignore | 3 + .../AppIcon.appiconset/AppIcon-20x20@1x.png | Bin 0 -> 1036 bytes .../AppIcon.appiconset/AppIcon-20x20@2x-1.png | Bin 0 -> 2216 bytes .../AppIcon.appiconset/AppIcon-20x20@2x.png | Bin 0 -> 2216 bytes .../AppIcon.appiconset/AppIcon-20x20@3x.png | Bin 0 -> 3512 bytes .../AppIcon.appiconset/AppIcon-29x29@1x.png | Bin 0 -> 1568 bytes .../AppIcon.appiconset/AppIcon-29x29@2x-1.png | Bin 0 -> 3394 bytes .../AppIcon.appiconset/AppIcon-29x29@2x.png | Bin 0 -> 3394 bytes .../AppIcon.appiconset/AppIcon-29x29@3x.png | Bin 0 -> 5228 bytes .../AppIcon.appiconset/AppIcon-40x40@1x.png | Bin 0 -> 2216 bytes .../AppIcon.appiconset/AppIcon-40x40@2x-1.png | Bin 0 -> 4794 bytes .../AppIcon.appiconset/AppIcon-40x40@2x.png | Bin 0 -> 4794 bytes .../AppIcon.appiconset/AppIcon-40x40@3x.png | Bin 0 -> 7288 bytes .../AppIcon.appiconset/AppIcon-512@2x.png | Bin 0 -> 121323 bytes .../AppIcon.appiconset/AppIcon-60x60@2x.png | Bin 0 -> 7288 bytes .../AppIcon.appiconset/AppIcon-60x60@3x.png | Bin 0 -> 11091 bytes .../AppIcon.appiconset/AppIcon-76x76@1x.png | Bin 0 -> 4515 bytes .../AppIcon.appiconset/AppIcon-76x76@2x.png | Bin 0 -> 9257 bytes .../AppIcon-83.5x83.5@2x.png | Bin 0 -> 10219 bytes .../AppIcon.appiconset/Contents.json | 116 + .../gen/apple/Assets.xcassets/Contents.json | 6 + .../src-tauri/gen/apple/ExportOptions.plist | 8 + .../gen/apple/LaunchScreen.storyboard | 30 + .../examples/app/src-tauri/gen/apple/Podfile | 21 + .../deep-link-example/bindings/bindings.h | 8 + .../apple/Sources/deep-link-example/main.mm | 6 + .../project.pbxproj | 479 ++ .../contents.xcworkspacedata | 7 + .../xcshareddata/WorkspaceSettings.xcsettings | 10 + .../xcschemes/deep-link-example_iOS.xcscheme | 123 + .../apple/deep-link-example_iOS/Info.plist | 55 + .../deep-link-example_iOS.entitlements | 11 + .../app/src-tauri/gen/apple/project.yml | 88 + .../examples/app/src-tauri/icons/128x128.png | Bin 0 -> 11059 bytes .../app/src-tauri/icons/128x128@2x.png | Bin 0 -> 23137 bytes .../examples/app/src-tauri/icons/32x32.png | Bin 0 -> 2225 bytes .../examples/app/src-tauri/icons/icon.ico | Bin 0 -> 37710 bytes .../examples/app/src-tauri/icons/icon.png | Bin 0 -> 49979 bytes .../examples/app/src-tauri/server.js | 31 + .../examples/app/src-tauri/src/lib.rs | 49 + .../examples/app/src-tauri/src/main.rs | 11 + .../examples/app/src-tauri/tauri.conf.json | 59 + .../examples/app/src/assets/tauri.svg | 6 + .../examples/app/src/assets/typescript.svg | 25 + .../examples/app/src/assets/vite.svg | 1 + plugins/deep-link/examples/app/src/main.ts | 38 + plugins/deep-link/examples/app/src/styles.css | 109 + plugins/deep-link/examples/app/tsconfig.json | 18 + plugins/deep-link/examples/app/vite.config.ts | 38 + plugins/deep-link/guest-js/index.ts | 128 + plugins/deep-link/package.json | 33 + .../autogenerated/commands/get_current.toml | 13 + .../autogenerated/commands/is_registered.toml | 13 + .../autogenerated/commands/register.toml | 13 + .../autogenerated/commands/unregister.toml | 13 + .../permissions/autogenerated/reference.md | 121 + plugins/deep-link/permissions/default.toml | 4 + .../deep-link/permissions/schemas/schema.json | 354 ++ plugins/deep-link/rollup.config.js | 7 + plugins/deep-link/src/commands.rs | 46 + plugins/deep-link/src/config.rs | 128 + plugins/deep-link/src/error.rs | 61 + plugins/deep-link/src/lib.rs | 601 +++ plugins/deep-link/src/template.desktop | 7 + plugins/deep-link/tsconfig.json | 7 + plugins/dialog/CHANGELOG.md | 341 ++ plugins/dialog/Cargo.toml | 58 + plugins/dialog/LICENSE.spdx | 20 + plugins/dialog/LICENSE_APACHE-2.0 | 177 + plugins/dialog/LICENSE_MIT | 21 + plugins/dialog/README.md | 107 + plugins/dialog/SECURITY.md | 23 + plugins/dialog/android/.gitignore | 2 + plugins/dialog/android/build.gradle.kts | 35 + plugins/dialog/android/consumer-rules.pro | 21 + plugins/dialog/android/settings.gradle | 2 + .../java/ExampleInstrumentedTest.kt | 28 + .../android/src/main/AndroidManifest.xml | 3 + .../android/src/main/java/DialogPlugin.kt | 246 + .../android/src/main/java/FilePickerUtils.kt | 230 + .../android/src/test/java/ExampleUnitTest.kt | 21 + plugins/dialog/api-iife.js | 1 + plugins/dialog/banner.png | Bin 0 -> 57224 bytes plugins/dialog/build.rs | 18 + plugins/dialog/guest-js/index.ts | 554 +++ plugins/dialog/guest-js/init.ts | 25 + plugins/dialog/ios/.gitignore | 10 + plugins/dialog/ios/Package.swift | 34 + plugins/dialog/ios/README.md | 3 + plugins/dialog/ios/Sources/DialogPlugin.swift | 313 ++ .../ios/Sources/FilePickerController.swift | 253 + .../ios/Tests/PluginTests/PluginTests.swift | 12 + plugins/dialog/package.json | 29 + plugins/dialog/permissions/ask.toml | 11 + .../autogenerated/commands/message.toml | 13 + .../autogenerated/commands/open.toml | 13 + .../autogenerated/commands/save.toml | 13 + .../permissions/autogenerated/reference.md | 154 + plugins/dialog/permissions/confirm.toml | 11 + plugins/dialog/permissions/default.toml | 14 + .../dialog/permissions/schemas/schema.json | 366 ++ plugins/dialog/rollup.config.js | 22 + plugins/dialog/src/commands.rs | 290 ++ plugins/dialog/src/desktop.rs | 257 + plugins/dialog/src/error.rs | 40 + plugins/dialog/src/init-iife.js | 1 + plugins/dialog/src/lib.rs | 786 +++ plugins/dialog/src/mobile.rs | 127 + plugins/dialog/src/models.rs | 109 + plugins/dialog/test/tauri.conf.json | 21 + plugins/dialog/tsconfig.json | 7 + plugins/fs/CHANGELOG.md | 232 + plugins/fs/Cargo.toml | 59 + plugins/fs/LICENSE.spdx | 20 + plugins/fs/LICENSE_APACHE-2.0 | 177 + plugins/fs/LICENSE_MIT | 21 + plugins/fs/README.md | 91 + plugins/fs/SECURITY.md | 49 + plugins/fs/android/.gitignore | 2 + plugins/fs/android/build.gradle.kts | 36 + plugins/fs/android/consumer-rules.pro | 21 + plugins/fs/android/settings.gradle | 31 + .../java/ExampleInstrumentedTest.kt | 28 + .../fs/android/src/main/AndroidManifest.xml | 3 + plugins/fs/android/src/main/java/FsPlugin.kt | 93 + .../android/src/test/java/ExampleUnitTest.kt | 21 + plugins/fs/api-iife.js | 1 + plugins/fs/banner.png | Bin 0 -> 50877 bytes plugins/fs/build.rs | 269 + plugins/fs/guest-js/index.ts | 1649 +++++++ plugins/fs/package.json | 30 + plugins/fs/permissions/app.toml | 114 + .../base-directories/appcache.toml | 82 + .../base-directories/appconfig.toml | 82 + .../base-directories/appdata.toml | 82 + .../base-directories/applocaldata.toml | 82 + .../base-directories/applog.toml | 82 + .../autogenerated/base-directories/audio.toml | 82 + .../autogenerated/base-directories/cache.toml | 82 + .../base-directories/config.toml | 82 + .../autogenerated/base-directories/data.toml | 82 + .../base-directories/desktop.toml | 82 + .../base-directories/document.toml | 82 + .../base-directories/download.toml | 82 + .../autogenerated/base-directories/exe.toml | 82 + .../autogenerated/base-directories/font.toml | 82 + .../autogenerated/base-directories/home.toml | 82 + .../base-directories/localdata.toml | 82 + .../autogenerated/base-directories/log.toml | 82 + .../base-directories/picture.toml | 82 + .../base-directories/public.toml | 82 + .../base-directories/resource.toml | 82 + .../base-directories/runtime.toml | 82 + .../autogenerated/base-directories/temp.toml | 82 + .../base-directories/template.toml | 82 + .../autogenerated/base-directories/video.toml | 82 + .../autogenerated/commands/copy_file.toml | 13 + .../autogenerated/commands/create.toml | 13 + .../autogenerated/commands/exists.toml | 13 + .../autogenerated/commands/fstat.toml | 13 + .../autogenerated/commands/ftruncate.toml | 13 + .../autogenerated/commands/lstat.toml | 13 + .../autogenerated/commands/mkdir.toml | 13 + .../autogenerated/commands/open.toml | 13 + .../autogenerated/commands/read.toml | 13 + .../autogenerated/commands/read_dir.toml | 13 + .../autogenerated/commands/read_file.toml | 13 + .../commands/read_text_file.toml | 13 + .../commands/read_text_file_lines.toml | 22 + .../commands/read_text_file_lines_next.toml | 13 + .../autogenerated/commands/remove.toml | 13 + .../autogenerated/commands/rename.toml | 13 + .../autogenerated/commands/seek.toml | 13 + .../autogenerated/commands/size.toml | 13 + ...rt_accessing_security_scoped_resource.toml | 13 + .../autogenerated/commands/stat.toml | 13 + ...op_accessing_security_scoped_resource.toml | 13 + .../autogenerated/commands/truncate.toml | 13 + .../autogenerated/commands/unwatch.toml | 13 + .../autogenerated/commands/watch.toml | 13 + .../autogenerated/commands/write.toml | 13 + .../autogenerated/commands/write_file.toml | 23 + .../commands/write_text_file.toml | 13 + .../fs/permissions/autogenerated/reference.md | 3866 +++++++++++++++ .../permissions/create-app-specific-dirs.toml | 8 + plugins/fs/permissions/default.toml | 35 + plugins/fs/permissions/deny-default.toml | 6 + plugins/fs/permissions/deny-webview-data.toml | 48 + plugins/fs/permissions/read-all.toml | 21 + .../read-app-specific-dirs-recursive.toml | 17 + plugins/fs/permissions/read-dirs.toml | 6 + plugins/fs/permissions/read-files.toml | 19 + plugins/fs/permissions/read-meta.toml | 6 + plugins/fs/permissions/scope.toml | 28 + plugins/fs/permissions/write-all.toml | 17 + plugins/fs/permissions/write-files.toml | 16 + plugins/fs/rollup.config.js | 7 + plugins/fs/src/android.rs | 92 + plugins/fs/src/commands.rs | 1885 +++++++ plugins/fs/src/config.rs | 19 + plugins/fs/src/desktop.rs | 44 + plugins/fs/src/error.rs | 51 + plugins/fs/src/file_path.rs | 308 ++ plugins/fs/src/ios.rs | 142 + plugins/fs/src/lib.rs | 632 +++ plugins/fs/src/models.rs | 18 + plugins/fs/src/scope.rs | 19 + plugins/fs/src/watcher.rs | 104 + plugins/fs/tsconfig.json | 7 + plugins/geolocation/CHANGELOG.md | 79 + plugins/geolocation/Cargo.toml | 37 + plugins/geolocation/LICENSE.spdx | 20 + plugins/geolocation/LICENSE_APACHE-2.0 | 177 + plugins/geolocation/LICENSE_MIT | 21 + plugins/geolocation/README.md | 173 + plugins/geolocation/SECURITY.md | 23 + plugins/geolocation/android/.gitignore | 2 + plugins/geolocation/android/build.gradle.kts | 36 + .../geolocation/android/consumer-rules.pro | 21 + plugins/geolocation/android/settings.gradle | 2 + .../java/ExampleInstrumentedTest.kt | 28 + .../android/src/main/AndroidManifest.xml | 6 + .../android/src/main/java/Geolocation.kt | 144 + .../src/main/java/GeolocationPlugin.kt | 172 + .../android/src/test/java/ExampleUnitTest.kt | 21 + plugins/geolocation/api-iife.js | 1 + plugins/geolocation/build.rs | 24 + .../geolocation/contributors/crabnebula.svg | 31 + plugins/geolocation/contributors/rescue.png | Bin 0 -> 8071 bytes plugins/geolocation/guest-js/index.ts | 239 + plugins/geolocation/ios/.gitignore | 10 + plugins/geolocation/ios/Package.swift | 34 + plugins/geolocation/ios/README.md | 3 + .../ios/Sources/GeolocationPlugin.swift | 250 + .../ios/Tests/PluginTests/PluginTests.swift | 12 + plugins/geolocation/package.json | 29 + .../commands/check_permissions.toml | 13 + .../commands/clear_permissions.toml | 13 + .../autogenerated/commands/clear_watch.toml | 13 + .../commands/get_current_position.toml | 13 + .../commands/request_permissions.toml | 13 + .../commands/watch_position.toml | 13 + .../permissions/autogenerated/reference.md | 165 + .../permissions/schemas/schema.json | 372 ++ plugins/geolocation/rollup.config.js | 7 + plugins/geolocation/src/commands.rs | 42 + plugins/geolocation/src/desktop.rs | 100 + plugins/geolocation/src/error.rs | 33 + plugins/geolocation/src/lib.rs | 65 + plugins/geolocation/src/mobile.rs | 123 + plugins/geolocation/src/models.rs | 106 + plugins/geolocation/tsconfig.json | 7 + plugins/global-shortcut/CHANGELOG.md | 126 + plugins/global-shortcut/Cargo.toml | 37 + plugins/global-shortcut/LICENSE.spdx | 20 + plugins/global-shortcut/LICENSE_APACHE-2.0 | 177 + plugins/global-shortcut/LICENSE_MIT | 21 + plugins/global-shortcut/README.md | 119 + plugins/global-shortcut/SECURITY.md | 23 + plugins/global-shortcut/api-iife.js | 1 + plugins/global-shortcut/banner.png | Bin 0 -> 64995 bytes plugins/global-shortcut/build.rs | 11 + plugins/global-shortcut/guest-js/index.ts | 131 + plugins/global-shortcut/package.json | 29 + .../autogenerated/commands/is_registered.toml | 13 + .../autogenerated/commands/register.toml | 13 + .../autogenerated/commands/register_all.toml | 13 + .../autogenerated/commands/unregister.toml | 13 + .../commands/unregister_all.toml | 13 + .../permissions/autogenerated/reference.md | 146 + .../global-shortcut/permissions/default.toml | 10 + .../permissions/schemas/schema.json | 366 ++ plugins/global-shortcut/rollup.config.js | 7 + plugins/global-shortcut/src/error.rs | 44 + plugins/global-shortcut/src/lib.rs | 497 ++ plugins/global-shortcut/tsconfig.json | 7 + plugins/haptics/CHANGELOG.md | 70 + plugins/haptics/Cargo.toml | 37 + plugins/haptics/LICENSE.spdx | 20 + plugins/haptics/LICENSE_APACHE-2.0 | 177 + plugins/haptics/LICENSE_MIT | 21 + plugins/haptics/README.md | 135 + plugins/haptics/SECURITY.md | 23 + plugins/haptics/android/.gitignore | 2 + plugins/haptics/android/build.gradle.kts | 36 + plugins/haptics/android/consumer-rules.pro | 21 + plugins/haptics/android/settings.gradle | 2 + .../java/ExampleInstrumentedTest.kt | 28 + .../android/src/main/AndroidManifest.xml | 4 + .../android/src/main/java/HapticsPlugin.kt | 143 + .../android/src/main/java/patterns/Impact.kt | 35 + .../src/main/java/patterns/Notification.kt | 23 + .../android/src/main/java/patterns/Pattern.kt | 11 + .../src/main/java/patterns/Selection.kt | 11 + .../android/src/test/java/ExampleUnitTest.kt | 21 + plugins/haptics/api-iife.js | 1 + plugins/haptics/build.rs | 23 + plugins/haptics/contributors/crabnebula.svg | 31 + plugins/haptics/contributors/rescue.png | Bin 0 -> 8071 bytes plugins/haptics/guest-js/bindings.ts | 154 + plugins/haptics/guest-js/index.ts | 104 + plugins/haptics/ios/.gitignore | 10 + plugins/haptics/ios/Package.swift | 34 + plugins/haptics/ios/README.md | 3 + .../haptics/ios/Sources/HapticsPlugin.swift | 133 + .../ios/Tests/PluginTests/PluginTests.swift | 12 + plugins/haptics/package.json | 29 + .../commands/impact_feedback.toml | 13 + .../commands/notification_feedback.toml | 13 + .../commands/selection_feedback.toml | 13 + .../autogenerated/commands/vibrate.toml | 13 + .../permissions/autogenerated/reference.md | 113 + .../haptics/permissions/schemas/schema.json | 348 ++ plugins/haptics/rollup.config.js | 7 + plugins/haptics/src/commands.rs | 33 + plugins/haptics/src/desktop.rs | 44 + plugins/haptics/src/error.rs | 35 + plugins/haptics/src/lib.rs | 64 + plugins/haptics/src/mobile.rs | 105 + plugins/haptics/src/models.rs | 54 + plugins/haptics/tsconfig.json | 7 + plugins/http/CHANGELOG.md | 352 ++ plugins/http/Cargo.toml | 80 + plugins/http/LICENSE.spdx | 20 + plugins/http/LICENSE_APACHE-2.0 | 177 + plugins/http/LICENSE_MIT | 21 + plugins/http/README.md | 93 + plugins/http/SECURITY.md | 23 + plugins/http/api-iife.js | 1 + plugins/http/banner.png | Bin 0 -> 52113 bytes plugins/http/build.rs | 73 + plugins/http/guest-js/index.ts | 347 ++ plugins/http/package.json | 29 + .../autogenerated/commands/fetch.toml | 13 + .../autogenerated/commands/fetch_cancel.toml | 13 + .../commands/fetch_cancel_body.toml | 13 + .../commands/fetch_read_body.toml | 13 + .../autogenerated/commands/fetch_send.toml | 13 + .../permissions/autogenerated/reference.md | 160 + plugins/http/permissions/default.toml | 23 + plugins/http/permissions/schemas/schema.json | 366 ++ plugins/http/rollup.config.js | 7 + plugins/http/src/commands.rs | 718 +++ plugins/http/src/config.rs | 47 + plugins/http/src/error.rs | 84 + plugins/http/src/lib.rs | 160 + plugins/http/src/reqwest_cookie_store.rs | 133 + plugins/http/src/scope.rs | 230 + plugins/http/tsconfig.json | 7 + plugins/localhost/CHANGELOG.md | 113 + plugins/localhost/Cargo.toml | 26 + plugins/localhost/LICENSE.spdx | 20 + plugins/localhost/LICENSE_APACHE-2.0 | 177 + plugins/localhost/LICENSE_MIT | 21 + plugins/localhost/README.md | 108 + plugins/localhost/SECURITY.md | 23 + plugins/localhost/banner.png | Bin 0 -> 45169 bytes plugins/localhost/src/lib.rs | 174 + plugins/log/CHANGELOG.md | 188 + plugins/log/Cargo.toml | 58 + plugins/log/LICENSE.spdx | 20 + plugins/log/LICENSE_APACHE-2.0 | 177 + plugins/log/LICENSE_MIT | 21 + plugins/log/README.md | 132 + plugins/log/SECURITY.md | 56 + plugins/log/api-iife.js | 1 + plugins/log/banner.png | Bin 0 -> 38416 bytes plugins/log/build.rs | 12 + plugins/log/guest-js/index.ts | 344 ++ plugins/log/ios/.gitignore | 10 + plugins/log/ios/Package.swift | 34 + plugins/log/ios/README.md | 3 + plugins/log/ios/Sources/LogPlugin.swift | 18 + plugins/log/package.json | 30 + .../autogenerated/commands/log.toml | 13 + .../permissions/autogenerated/reference.md | 43 + plugins/log/permissions/default.toml | 4 + plugins/log/permissions/schemas/schema.json | 318 ++ plugins/log/rollup.config.js | 7 + plugins/log/src/commands.rs | 73 + plugins/log/src/lib.rs | 925 ++++ plugins/log/tsconfig.json | 7 + plugins/mirrors.txt | 20 + plugins/nfc/CHANGELOG.md | 116 + plugins/nfc/Cargo.toml | 31 + plugins/nfc/LICENSE.spdx | 20 + plugins/nfc/LICENSE_APACHE-2.0 | 177 + plugins/nfc/LICENSE_MIT | 21 + plugins/nfc/README.md | 112 + plugins/nfc/SECURITY.md | 23 + plugins/nfc/android/.gitignore | 2 + plugins/nfc/android/build.gradle.kts | 36 + plugins/nfc/android/consumer-rules.pro | 21 + plugins/nfc/android/settings.gradle | 2 + .../java/ExampleInstrumentedTest.kt | 28 + .../nfc/android/src/main/AndroidManifest.xml | 4 + .../nfc/android/src/main/java/NfcPlugin.kt | 519 ++ .../src/main/res/xml/nfc_tech_filter.xml | 13 + .../android/src/test/java/ExampleUnitTest.kt | 21 + plugins/nfc/api-iife.js | 1 + plugins/nfc/build.rs | 45 + plugins/nfc/contributors/crabnebula.svg | 31 + plugins/nfc/contributors/impierce.svg | 21 + plugins/nfc/guest-js/index.ts | 469 ++ plugins/nfc/ios/.gitignore | 10 + plugins/nfc/ios/Package.swift | 34 + plugins/nfc/ios/README.md | 3 + plugins/nfc/ios/Sources/NfcPlugin.swift | 523 ++ .../ios/Tests/PluginTests/PluginTests.swift | 12 + plugins/nfc/package.json | 30 + .../autogenerated/commands/is_available.toml | 13 + .../autogenerated/commands/scan.toml | 13 + .../autogenerated/commands/write.toml | 13 + .../permissions/autogenerated/reference.md | 103 + plugins/nfc/permissions/default.toml | 15 + plugins/nfc/permissions/schemas/schema.json | 342 ++ plugins/nfc/rollup.config.js | 7 + plugins/nfc/src/error.rs | 34 + plugins/nfc/src/lib.rs | 122 + plugins/nfc/src/mobile.rs | 11 + plugins/nfc/src/models.rs | 195 + plugins/nfc/tsconfig.json | 7 + plugins/notification/CHANGELOG.md | 165 + plugins/notification/Cargo.toml | 58 + plugins/notification/LICENSE.spdx | 20 + plugins/notification/LICENSE_APACHE-2.0 | 177 + plugins/notification/LICENSE_MIT | 21 + plugins/notification/README.md | 161 + plugins/notification/SECURITY.md | 23 + plugins/notification/android/.gitignore | 2 + plugins/notification/android/build.gradle.kts | 36 + .../notification/android/consumer-rules.pro | 21 + plugins/notification/android/settings.gradle | 2 + .../java/ExampleInstrumentedTest.kt | 28 + .../android/src/main/AndroidManifest.xml | 20 + .../android/src/main/java/AssetUtils.kt | 29 + .../android/src/main/java/ChannelManager.kt | 151 + .../android/src/main/java/Notification.kt | 95 + .../src/main/java/NotificationAttachment.kt | 52 + .../src/main/java/NotificationPlugin.kt | 286 ++ .../src/main/java/NotificationSchedule.kt | 369 ++ .../src/main/java/NotificationStorage.kt | 113 + .../src/main/java/TauriNotificationManager.kt | 581 +++ .../src/main/res/drawable/ic_transparent.xml | 12 + .../android/src/test/java/ExampleUnitTest.kt | 21 + plugins/notification/api-iife.js | 1 + plugins/notification/banner.png | Bin 0 -> 60579 bytes plugins/notification/build.rs | 36 + plugins/notification/guest-js/index.ts | 967 ++++ plugins/notification/guest-js/init.ts | 107 + plugins/notification/ios/.gitignore | 10 + plugins/notification/ios/Package.swift | 34 + plugins/notification/ios/README.md | 3 + .../ios/Sources/Notification.swift | 286 ++ .../ios/Sources/NotificationCategory.swift | 97 + .../ios/Sources/NotificationHandler.swift | 118 + .../ios/Sources/NotificationManager.swift | 47 + .../ios/Sources/NotificationPlugin.swift | 290 ++ .../ios/Tests/PluginTests/PluginTests.swift | 12 + plugins/notification/package.json | 29 + .../autogenerated/commands/batch.toml | 13 + .../autogenerated/commands/cancel.toml | 13 + .../commands/check_permissions.toml | 13 + .../commands/create_channel.toml | 13 + .../commands/delete_channel.toml | 13 + .../autogenerated/commands/get_active.toml | 13 + .../autogenerated/commands/get_pending.toml | 13 + .../commands/is_permission_granted.toml | 13 + .../autogenerated/commands/list_channels.toml | 13 + .../autogenerated/commands/notify.toml | 13 + .../commands/permission_state.toml | 13 + .../commands/register_action_types.toml | 13 + .../commands/register_listener.toml | 13 + .../autogenerated/commands/remove_active.toml | 13 + .../commands/remove_listener.toml | 13 + .../commands/request_permission.toml | 13 + .../autogenerated/commands/show.toml | 13 + .../permissions/autogenerated/reference.md | 480 ++ plugins/notification/permissions/default.toml | 31 + .../permissions/schemas/schema.json | 510 ++ plugins/notification/rollup.config.js | 22 + plugins/notification/src/commands.rs | 39 + plugins/notification/src/desktop.rs | 330 ++ plugins/notification/src/error.rs | 33 + plugins/notification/src/init-iife.js | 1 + plugins/notification/src/lib.rs | 265 + plugins/notification/src/mobile.rs | 202 + plugins/notification/src/models.rs | 914 ++++ plugins/notification/test/tauri.conf.json | 21 + plugins/notification/tsconfig.json | 7 + plugins/opener/CHANGELOG.md | 80 + plugins/opener/Cargo.toml | 67 + plugins/opener/LICENSE.spdx | 20 + plugins/opener/LICENSE_APACHE-2.0 | 177 + plugins/opener/LICENSE_MIT | 21 + plugins/opener/README.md | 143 + plugins/opener/SECURITY.md | 23 + plugins/opener/android/.gitignore | 2 + plugins/opener/android/build.gradle.kts | 31 + plugins/opener/android/consumer-rules.pro | 21 + plugins/opener/android/settings.gradle | 2 + .../android/src/main/AndroidManifest.xml | 3 + .../android/src/main/java/OpenerPlugin.kt | 44 + plugins/opener/api-iife.js | 1 + plugins/opener/build.rs | 132 + plugins/opener/guest-js/index.ts | 99 + plugins/opener/guest-js/init.ts | 70 + plugins/opener/ios/Package.resolved | 16 + plugins/opener/ios/Package.swift | 34 + plugins/opener/ios/Sources/OpenerPlugin.swift | 46 + plugins/opener/package.json | 30 + .../permissions/allow-default-urls.toml | 17 + .../autogenerated/commands/open_path.toml | 13 + .../autogenerated/commands/open_url.toml | 13 + .../commands/reveal_item_in_dir.toml | 13 + .../permissions/autogenerated/reference.md | 111 + plugins/opener/permissions/default.toml | 10 + .../opener/permissions/schemas/schema.json | 348 ++ plugins/opener/rollup.config.js | 22 + plugins/opener/src/commands.rs | 76 + plugins/opener/src/config.rs | 19 + plugins/opener/src/error.rs | 82 + plugins/opener/src/init-iife.js | 1 + plugins/opener/src/lib.rs | 269 + plugins/opener/src/open.rs | 61 + plugins/opener/src/reveal_item_in_dir.rs | 311 ++ plugins/opener/src/scope.rs | 141 + plugins/opener/src/scope_entry.rs | 32 + plugins/opener/src/windows_shell_path.rs | 255 + plugins/opener/tsconfig.json | 7 + plugins/os/CHANGELOG.md | 124 + plugins/os/Cargo.toml | 38 + plugins/os/LICENSE.spdx | 20 + plugins/os/LICENSE_APACHE-2.0 | 177 + plugins/os/LICENSE_MIT | 21 + plugins/os/README.md | 90 + plugins/os/SECURITY.md | 23 + plugins/os/api-iife.js | 1 + plugins/os/banner.png | Bin 0 -> 57150 bytes plugins/os/build.rs | 20 + plugins/os/guest-js/index.ts | 225 + plugins/os/package.json | 29 + .../autogenerated/commands/arch.toml | 13 + .../autogenerated/commands/exe_extension.toml | 13 + .../autogenerated/commands/family.toml | 13 + .../autogenerated/commands/hostname.toml | 13 + .../autogenerated/commands/locale.toml | 13 + .../autogenerated/commands/os_type.toml | 13 + .../autogenerated/commands/platform.toml | 13 + .../autogenerated/commands/version.toml | 13 + .../os/permissions/autogenerated/reference.md | 237 + plugins/os/permissions/default.toml | 23 + plugins/os/permissions/schemas/schema.json | 402 ++ plugins/os/rollup.config.js | 7 + plugins/os/src/commands.rs | 13 + plugins/os/src/error.rs | 20 + plugins/os/src/init.js | 16 + plugins/os/src/lib.rs | 150 + plugins/os/tsconfig.json | 7 + plugins/persisted-scope/CHANGELOG.md | 350 ++ plugins/persisted-scope/Cargo.toml | 30 + plugins/persisted-scope/LICENSE.spdx | 20 + plugins/persisted-scope/LICENSE_APACHE-2.0 | 177 + plugins/persisted-scope/LICENSE_MIT | 21 + plugins/persisted-scope/README.md | 75 + plugins/persisted-scope/SECURITY.md | 23 + plugins/persisted-scope/banner.png | Bin 0 -> 47372 bytes plugins/persisted-scope/src/lib.rs | 267 + plugins/positioner/CHANGELOG.md | 187 + plugins/positioner/Cargo.toml | 38 + plugins/positioner/LICENSE.spdx | 20 + plugins/positioner/LICENSE_APACHE-2.0 | 177 + plugins/positioner/LICENSE_MIT | 21 + plugins/positioner/README.md | 148 + plugins/positioner/SECURITY.md | 23 + plugins/positioner/api-iife.js | 1 + plugins/positioner/banner.png | Bin 0 -> 43986 bytes plugins/positioner/build.rs | 15 + plugins/positioner/guest-js/index.ts | 140 + plugins/positioner/package.json | 30 + .../autogenerated/commands/move_window.toml | 13 + .../commands/move_window_constrained.toml | 13 + .../commands/set_tray_icon_state.toml | 13 + .../permissions/autogenerated/reference.md | 97 + plugins/positioner/permissions/default.toml | 8 + .../permissions/schemas/schema.json | 342 ++ plugins/positioner/rollup.config.js | 7 + plugins/positioner/src/ext.rs | 346 ++ plugins/positioner/src/lib.rs | 114 + plugins/positioner/tsconfig.json | 7 + plugins/process/CHANGELOG.md | 106 + plugins/process/Cargo.toml | 30 + plugins/process/LICENSE.spdx | 20 + plugins/process/LICENSE_APACHE-2.0 | 177 + plugins/process/LICENSE_MIT | 21 + plugins/process/README.md | 93 + plugins/process/SECURITY.md | 23 + plugins/process/api-iife.js | 1 + plugins/process/banner.png | Bin 0 -> 60700 bytes plugins/process/build.rs | 11 + plugins/process/guest-js/index.ts | 45 + plugins/process/package.json | 29 + .../autogenerated/commands/exit.toml | 13 + .../autogenerated/commands/restart.toml | 13 + .../permissions/autogenerated/reference.md | 76 + plugins/process/permissions/default.toml | 14 + .../process/permissions/schemas/schema.json | 330 ++ plugins/process/rollup.config.js | 7 + plugins/process/src/commands.rs | 15 + plugins/process/src/lib.rs | 24 + plugins/process/tsconfig.json | 7 + plugins/shell/CHANGELOG.md | 159 + plugins/shell/Cargo.toml | 49 + plugins/shell/LICENSE.spdx | 20 + plugins/shell/LICENSE_APACHE-2.0 | 177 + plugins/shell/LICENSE_MIT | 21 + plugins/shell/README.md | 90 + plugins/shell/SECURITY.md | 23 + plugins/shell/android/.gitignore | 2 + plugins/shell/android/build.gradle.kts | 30 + plugins/shell/android/consumer-rules.pro | 21 + plugins/shell/android/settings.gradle | 2 + .../android/src/main/AndroidManifest.xml | 3 + .../android/src/main/java/ShellPlugin.kt | 30 + plugins/shell/api-iife.js | 1 + plugins/shell/banner.png | Bin 0 -> 52105 bytes plugins/shell/build.rs | 189 + plugins/shell/guest-js/index.ts | 928 ++++ plugins/shell/guest-js/init.ts | 51 + plugins/shell/ios/Package.resolved | 16 + plugins/shell/ios/Package.swift | 34 + plugins/shell/ios/Sources/ShellPlugin.swift | 34 + plugins/shell/package.json | 29 + .../autogenerated/commands/execute.toml | 13 + .../autogenerated/commands/kill.toml | 13 + .../autogenerated/commands/open.toml | 13 + .../autogenerated/commands/spawn.toml | 13 + .../autogenerated/commands/stdin_write.toml | 13 + .../permissions/autogenerated/reference.md | 154 + plugins/shell/permissions/default.toml | 15 + plugins/shell/permissions/schemas/schema.json | 366 ++ plugins/shell/rollup.config.js | 22 + plugins/shell/src/commands.rs | 320 ++ plugins/shell/src/config.rs | 39 + plugins/shell/src/error.rs | 56 + plugins/shell/src/init-iife.js | 1 + plugins/shell/src/lib.rs | 195 + plugins/shell/src/open.rs | 138 + plugins/shell/src/process/mod.rs | 658 +++ plugins/shell/src/scope.rs | 322 ++ plugins/shell/src/scope_entry.rs | 77 + plugins/shell/test/test.txt | 1 + plugins/shell/tsconfig.json | 7 + plugins/single-instance/CHANGELOG.md | 268 + plugins/single-instance/Cargo.toml | 46 + plugins/single-instance/LICENSE.spdx | 20 + plugins/single-instance/LICENSE_APACHE-2.0 | 177 + plugins/single-instance/LICENSE_MIT | 21 + plugins/single-instance/README.md | 90 + plugins/single-instance/SECURITY.md | 23 + plugins/single-instance/banner.png | Bin 0 -> 47361 bytes .../examples/vanilla/.gitignore | 3 + .../examples/vanilla/package.json | 14 + .../examples/vanilla/public/index.html | 5 + .../examples/vanilla/src-tauri/.gitignore | 6 + .../examples/vanilla/src-tauri/Cargo.toml | 18 + .../examples/vanilla/src-tauri/build.rs | 7 + .../vanilla/src-tauri/icons/128x128.png | Bin 0 -> 3512 bytes .../vanilla/src-tauri/icons/128x128@2x.png | Bin 0 -> 7012 bytes .../vanilla/src-tauri/icons/32x32.png | Bin 0 -> 974 bytes .../examples/vanilla/src-tauri/icons/icon.ico | Bin 0 -> 86642 bytes .../examples/vanilla/src-tauri/icons/icon.png | Bin 0 -> 14183 bytes .../examples/vanilla/src-tauri/src/main.rs | 22 + .../vanilla/src-tauri/tauri.conf.json | 45 + plugins/single-instance/src/lib.rs | 134 + .../src/platform_impl/linux.rs | 112 + .../src/platform_impl/macos.rs | 132 + .../src/platform_impl/windows.rs | 267 + plugins/single-instance/src/semver_compat.rs | 19 + plugins/sql/CHANGELOG.md | 142 + plugins/sql/Cargo.toml | 50 + plugins/sql/LICENSE.spdx | 20 + plugins/sql/LICENSE_APACHE-2.0 | 177 + plugins/sql/LICENSE_MIT | 21 + plugins/sql/README.md | 211 + plugins/sql/SECURITY.md | 23 + plugins/sql/api-iife.js | 1 + plugins/sql/banner.png | Bin 0 -> 40282 bytes plugins/sql/build.rs | 11 + plugins/sql/guest-js/index.ts | 226 + plugins/sql/package.json | 30 + .../autogenerated/commands/close.toml | 13 + .../autogenerated/commands/execute.toml | 13 + .../autogenerated/commands/load.toml | 13 + .../autogenerated/commands/select.toml | 13 + .../permissions/autogenerated/reference.md | 131 + plugins/sql/permissions/default.toml | 16 + plugins/sql/permissions/schemas/schema.json | 354 ++ plugins/sql/rollup.config.js | 7 + plugins/sql/src/commands.rs | 80 + plugins/sql/src/decode/mod.rs | 10 + plugins/sql/src/decode/mysql.rs | 101 + plugins/sql/src/decode/postgres.rs | 136 + plugins/sql/src/decode/sqlite.rs | 78 + plugins/sql/src/error.rs | 42 + plugins/sql/src/lib.rs | 252 + plugins/sql/src/wrapper.rs | 343 ++ plugins/sql/tsconfig.json | 7 + plugins/store/CHANGELOG.md | 153 + plugins/store/Cargo.toml | 43 + plugins/store/LICENSE.spdx | 20 + plugins/store/LICENSE_APACHE-2.0 | 177 + plugins/store/LICENSE_MIT | 21 + plugins/store/README.md | 157 + plugins/store/SECURITY.md | 23 + plugins/store/api-iife.js | 1 + plugins/store/banner.png | Bin 0 -> 40819 bytes plugins/store/build.rs | 26 + .../examples/AppSettingsManager/.gitignore | 24 + .../.vscode/extensions.json | 3 + .../examples/AppSettingsManager/README.md | 7 + .../examples/AppSettingsManager/index.html | 54 + .../examples/AppSettingsManager/package.json | 15 + .../AppSettingsManager/src-tauri/.gitignore | 4 + .../AppSettingsManager/src-tauri/Cargo.toml | 19 + .../AppSettingsManager/src-tauri/build.rs | 7 + .../src-tauri/icons/128x128.png | Bin 0 -> 3512 bytes .../src-tauri/icons/128x128@2x.png | Bin 0 -> 7012 bytes .../src-tauri/icons/32x32.png | Bin 0 -> 974 bytes .../src-tauri/icons/Square107x107Logo.png | Bin 0 -> 2863 bytes .../src-tauri/icons/Square142x142Logo.png | Bin 0 -> 3858 bytes .../src-tauri/icons/Square150x150Logo.png | Bin 0 -> 3966 bytes .../src-tauri/icons/Square284x284Logo.png | Bin 0 -> 7737 bytes .../src-tauri/icons/Square30x30Logo.png | Bin 0 -> 903 bytes .../src-tauri/icons/Square310x310Logo.png | Bin 0 -> 8591 bytes .../src-tauri/icons/Square44x44Logo.png | Bin 0 -> 1299 bytes .../src-tauri/icons/Square71x71Logo.png | Bin 0 -> 2011 bytes .../src-tauri/icons/Square89x89Logo.png | Bin 0 -> 2468 bytes .../src-tauri/icons/StoreLogo.png | Bin 0 -> 1523 bytes .../src-tauri/icons/icon.icns | Bin 0 -> 98451 bytes .../src-tauri/icons/icon.ico | Bin 0 -> 86642 bytes .../src-tauri/icons/icon.png | Bin 0 -> 14183 bytes .../src-tauri/src/app/mod.rs | 5 + .../src-tauri/src/app/settings.rs | 30 + .../AppSettingsManager/src-tauri/src/main.rs | 41 + .../src-tauri/tauri.conf.json | 34 + .../AppSettingsManager/src/assets/tauri.svg | 6 + .../src/assets/typescript.svg | 25 + .../AppSettingsManager/src/assets/vite.svg | 1 + .../examples/AppSettingsManager/src/main.ts | 9 + .../AppSettingsManager/src/styles.css | 109 + .../examples/AppSettingsManager/tsconfig.json | 23 + .../AppSettingsManager/vite.config.ts | 21 + plugins/store/guest-js/index.ts | 933 ++++ plugins/store/package.json | 30 + .../autogenerated/commands/clear.toml | 13 + .../autogenerated/commands/delete.toml | 13 + .../autogenerated/commands/entries.toml | 13 + .../autogenerated/commands/get.toml | 13 + .../autogenerated/commands/get_store.toml | 13 + .../autogenerated/commands/has.toml | 13 + .../autogenerated/commands/keys.toml | 13 + .../autogenerated/commands/length.toml | 13 + .../autogenerated/commands/load.toml | 13 + .../autogenerated/commands/reload.toml | 13 + .../autogenerated/commands/reset.toml | 13 + .../autogenerated/commands/save.toml | 13 + .../autogenerated/commands/set.toml | 13 + .../autogenerated/commands/values.toml | 13 + .../permissions/autogenerated/reference.md | 399 ++ plugins/store/permissions/default.toml | 28 + plugins/store/permissions/schemas/schema.json | 474 ++ plugins/store/rollup.config.js | 7 + plugins/store/src/error.rs | 47 + plugins/store/src/lib.rs | 483 ++ plugins/store/src/store.rs | 638 +++ plugins/store/tsconfig.json | 7 + plugins/stronghold/CHANGELOG.md | 111 + plugins/stronghold/Cargo.toml | 49 + plugins/stronghold/LICENSE.spdx | 20 + plugins/stronghold/LICENSE_APACHE-2.0 | 177 + plugins/stronghold/LICENSE_MIT | 21 + plugins/stronghold/README.md | 169 + plugins/stronghold/SECURITY.md | 23 + plugins/stronghold/api-iife.js | 1 + plugins/stronghold/banner.png | Bin 0 -> 43820 bytes plugins/stronghold/build.rs | 23 + plugins/stronghold/guest-js/index.ts | 768 +++ plugins/stronghold/package.json | 30 + .../autogenerated/commands/create_client.toml | 13 + .../autogenerated/commands/destroy.toml | 13 + .../commands/execute_procedure.toml | 13 + .../commands/get_store_record.toml | 13 + .../autogenerated/commands/initialize.toml | 13 + .../autogenerated/commands/load_client.toml | 13 + .../autogenerated/commands/remove_secret.toml | 13 + .../commands/remove_store_record.toml | 13 + .../autogenerated/commands/save.toml | 13 + .../autogenerated/commands/save_secret.toml | 13 + .../commands/save_store_record.toml | 13 + .../permissions/autogenerated/reference.md | 315 ++ plugins/stronghold/permissions/default.toml | 22 + .../permissions/schemas/schema.json | 438 ++ plugins/stronghold/rollup.config.js | 7 + plugins/stronghold/src/kdf.rs | 52 + plugins/stronghold/src/lib.rs | 533 ++ plugins/stronghold/src/stronghold.rs | 103 + plugins/stronghold/tsconfig.json | 7 + plugins/updater/CHANGELOG.md | 257 + plugins/updater/Cargo.toml | 86 + plugins/updater/LICENSE.spdx | 20 + plugins/updater/LICENSE_APACHE-2.0 | 177 + plugins/updater/LICENSE_MIT | 21 + plugins/updater/README.md | 103 + plugins/updater/SECURITY.md | 23 + plugins/updater/api-iife.js | 1 + plugins/updater/banner.png | Bin 0 -> 57959 bytes plugins/updater/build.rs | 25 + plugins/updater/guest-js/index.ts | 304 ++ plugins/updater/package.json | 29 + .../autogenerated/commands/check.toml | 13 + .../autogenerated/commands/download.toml | 13 + .../commands/download_and_install.toml | 13 + .../autogenerated/commands/install.toml | 13 + .../permissions/autogenerated/reference.md | 130 + plugins/updater/permissions/default.toml | 18 + .../updater/permissions/schemas/schema.json | 354 ++ plugins/updater/rollup.config.js | 7 + plugins/updater/src/commands.rs | 208 + plugins/updater/src/config.rs | 217 + plugins/updater/src/error.rs | 134 + plugins/updater/src/lib.rs | 299 ++ plugins/updater/src/updater.rs | 1980 ++++++++ plugins/updater/tests/app-updater/.gitignore | 1 + plugins/updater/tests/app-updater/Cargo.toml | 15 + plugins/updater/tests/app-updater/build.rs | 7 + .../tests/app-updater/icons/128x128.png | Bin 0 -> 11059 bytes .../tests/app-updater/icons/128x128@2x.png | Bin 0 -> 23137 bytes .../updater/tests/app-updater/icons/32x32.png | Bin 0 -> 2225 bytes .../updater/tests/app-updater/icons/icon.ico | Bin 0 -> 37710 bytes .../updater/tests/app-updater/icons/icon.png | Bin 0 -> 49979 bytes plugins/updater/tests/app-updater/src/main.rs | 57 + .../updater/tests/app-updater/tauri.conf.json | 33 + .../updater/tests/app-updater/tests/update.rs | 1101 +++++ .../tests/updater-migration/Cargo.toml | 15 + .../tests/updater-migration/icons/128x128.png | Bin 0 -> 11059 bytes .../updater-migration/icons/128x128@2x.png | Bin 0 -> 23137 bytes .../tests/updater-migration/icons/32x32.png | Bin 0 -> 2225 bytes .../tests/updater-migration/icons/icon.ico | Bin 0 -> 37710 bytes .../tests/updater-migration/icons/icon.png | Bin 0 -> 49979 bytes .../tests/updater-migration/tests/update.rs | 469 ++ .../tests/updater-migration/v1-app/.gitignore | 3 + .../tests/updater-migration/v1-app/Cargo.lock | 4361 +++++++++++++++++ .../tests/updater-migration/v1-app/Cargo.toml | 18 + .../tests/updater-migration/v1-app/build.rs | 7 + .../updater-migration/v1-app/package.json | 10 + .../updater-migration/v1-app/src/main.rs | 50 + .../updater-migration/v1-app/tauri.conf.json | 39 + .../tests/updater-migration/v2-app/.gitignore | 1 + .../tests/updater-migration/v2-app/Cargo.toml | 14 + .../tests/updater-migration/v2-app/build.rs | 7 + .../updater-migration/v2-app/src/main.rs | 58 + .../updater-migration/v2-app/tauri.conf.json | 33 + plugins/updater/tsconfig.json | 7 + plugins/upload/CHANGELOG.md | 137 + plugins/upload/Cargo.toml | 54 + plugins/upload/LICENSE.spdx | 20 + plugins/upload/LICENSE_APACHE-2.0 | 177 + plugins/upload/LICENSE_MIT | 21 + plugins/upload/README.md | 121 + plugins/upload/SECURITY.md | 23 + plugins/upload/api-iife.js | 1 + plugins/upload/banner.png | Bin 0 -> 41691 bytes plugins/upload/build.rs | 11 + plugins/upload/guest-js/index.ts | 167 + plugins/upload/package.json | 30 + .../autogenerated/commands/download.toml | 13 + .../autogenerated/commands/upload.toml | 13 + .../permissions/autogenerated/reference.md | 75 + plugins/upload/permissions/default.toml | 13 + .../upload/permissions/schemas/schema.json | 330 ++ plugins/upload/rollup.config.js | 7 + plugins/upload/src/lib.rs | 380 ++ plugins/upload/src/transfer_stats.rs | 55 + plugins/upload/test/download.txt | 1 + plugins/upload/test/upload.txt | 1 + plugins/upload/tsconfig.json | 7 + plugins/websocket/CHANGELOG.md | 126 + plugins/websocket/Cargo.toml | 53 + plugins/websocket/LICENSE.spdx | 20 + plugins/websocket/LICENSE_APACHE-2.0 | 177 + plugins/websocket/LICENSE_MIT | 21 + plugins/websocket/README.md | 95 + plugins/websocket/SECURITY.md | 23 + plugins/websocket/api-iife.js | 1 + plugins/websocket/banner.png | Bin 0 -> 39670 bytes plugins/websocket/build.rs | 11 + .../websocket/examples/tauri-app/.gitignore | 25 + .../websocket/examples/tauri-app/index.html | 20 + .../websocket/examples/tauri-app/package.json | 19 + .../examples/tauri-app/public/vite.svg | 1 + .../examples/tauri-app/src-tauri/.gitignore | 9 + .../examples/tauri-app/src-tauri/Cargo.toml | 17 + .../examples/tauri-app/src-tauri/build.rs | 7 + .../tauri-app/src-tauri/icons/128x128.png | Bin 0 -> 15920 bytes .../tauri-app/src-tauri/icons/128x128@2x.png | Bin 0 -> 37546 bytes .../tauri-app/src-tauri/icons/32x32.png | Bin 0 -> 2400 bytes .../src-tauri/icons/Square107x107Logo.png | Bin 0 -> 12659 bytes .../src-tauri/icons/Square142x142Logo.png | Bin 0 -> 18057 bytes .../src-tauri/icons/Square150x150Logo.png | Bin 0 -> 19312 bytes .../src-tauri/icons/Square284x284Logo.png | Bin 0 -> 41714 bytes .../src-tauri/icons/Square30x30Logo.png | Bin 0 -> 2132 bytes .../src-tauri/icons/Square310x310Logo.png | Bin 0 -> 45901 bytes .../src-tauri/icons/Square44x44Logo.png | Bin 0 -> 3763 bytes .../src-tauri/icons/Square71x71Logo.png | Bin 0 -> 7380 bytes .../src-tauri/icons/Square89x89Logo.png | Bin 0 -> 9831 bytes .../tauri-app/src-tauri/icons/StoreLogo.png | Bin 0 -> 4552 bytes .../tauri-app/src-tauri/icons/icon.ico | Bin 0 -> 57594 bytes .../tauri-app/src-tauri/icons/icon.png | Bin 0 -> 90074 bytes .../examples/tauri-app/src-tauri/rustfmt.toml | 13 + .../examples/tauri-app/src-tauri/src/main.rs | 42 + .../tauri-app/src-tauri/tauri.conf.json | 34 + .../websocket/examples/tauri-app/src/main.ts | 58 + .../examples/tauri-app/src/style.css | 73 + .../examples/tauri-app/src/typescript.svg | 1 + .../examples/tauri-app/src/vite-env.d.ts | 5 + .../examples/tauri-app/tsconfig.json | 23 + plugins/websocket/guest-js/index.ts | 227 + plugins/websocket/package.json | 29 + .../autogenerated/commands/connect.toml | 13 + .../autogenerated/commands/send.toml | 13 + .../permissions/autogenerated/reference.md | 70 + plugins/websocket/permissions/default.toml | 4 + .../websocket/permissions/schemas/schema.json | 330 ++ plugins/websocket/rollup.config.js | 7 + plugins/websocket/src/lib.rs | 331 ++ plugins/websocket/tsconfig.json | 7 + plugins/window-state/CHANGELOG.md | 165 + plugins/window-state/Cargo.toml | 35 + plugins/window-state/LICENSE.spdx | 20 + plugins/window-state/LICENSE_APACHE-2.0 | 177 + plugins/window-state/LICENSE_MIT | 21 + plugins/window-state/README.md | 122 + plugins/window-state/SECURITY.md | 23 + plugins/window-state/api-iife.js | 1 + plugins/window-state/banner.png | Bin 0 -> 46441 bytes plugins/window-state/build.rs | 11 + plugins/window-state/guest-js/index.ts | 106 + plugins/window-state/package.json | 30 + .../autogenerated/commands/filename.toml | 13 + .../autogenerated/commands/restore_state.toml | 13 + .../commands/save_window_state.toml | 13 + .../permissions/autogenerated/reference.md | 102 + plugins/window-state/permissions/default.toml | 17 + .../permissions/schemas/schema.json | 342 ++ plugins/window-state/rollup.config.js | 7 + plugins/window-state/src/cmd.rs | 48 + plugins/window-state/src/lib.rs | 575 +++ plugins/window-state/tsconfig.json | 7 + pnpm-workspace.yaml | 30 + renovate.json | 38 + shared/rollup.config.js | 104 + shared/template/Cargo.toml | 31 + shared/template/LICENSE.spdx | 20 + shared/template/LICENSE_APACHE-2.0 | 177 + shared/template/LICENSE_MIT | 21 + shared/template/README.md | 89 + shared/template/SECURITY.md | 23 + shared/template/android/.gitignore | 2 + shared/template/android/build.gradle.kts | 35 + shared/template/android/consumer-rules.pro | 21 + shared/template/android/settings.gradle | 2 + .../java/ExampleInstrumentedTest.kt | 28 + .../android/src/main/AndroidManifest.xml | 3 + .../android/src/main/java/ExamplePlugin.kt | 32 + .../android/src/test/java/ExampleUnitTest.kt | 21 + shared/template/build.rs | 18 + shared/template/guest-js/index.ts | 3 + shared/template/ios/.gitignore | 10 + shared/template/ios/Package.swift | 34 + shared/template/ios/README.md | 3 + .../template/ios/Sources/ExamplePlugin.swift | 24 + .../ios/Tests/PluginTests/PluginTests.swift | 12 + shared/template/package.json | 29 + shared/template/rollup.config.js | 7 + shared/template/src/commands.rs | 15 + shared/template/src/desktop.rs | 26 + shared/template/src/error.rs | 25 + shared/template/src/lib.rs | 52 + shared/template/src/mobile.rs | 41 + shared/template/src/models.rs | 17 + shared/template/tsconfig.json | 7 + taplo.toml | 1 + tsconfig.base.json | 13 + 1469 files changed, 115067 insertions(+) create mode 100644 .cargo/audit.toml create mode 100644 .changes/config.json create mode 100644 .changes/fix-autostart-windows-disable.md create mode 100644 .changes/fix-deep-link-linux-unregister.md create mode 100644 .changes/fix-geo-haptics-specta-skip.md create mode 100644 .changes/fix-notification-mobile-permissions.md create mode 100644 .changes/fix-notification-windows-permission.md create mode 100644 .changes/global-api-script-reuse-core.md create mode 100644 .changes/readme.md create mode 100644 .changes/update-docs.md create mode 100644 .github/CODEOWNERS create mode 100644 .github/CONTRIBUTING.md create mode 100644 .github/ISSUE_TEMPLATE/bug_report.yml create mode 100644 .github/ISSUE_TEMPLATE/config.yml create mode 100644 .github/ISSUE_TEMPLATE/docs_report.md create mode 100644 .github/ISSUE_TEMPLATE/feature_request.yml create mode 100644 .github/sponsors/crabnebula.svg create mode 100644 .github/sponsors/rescue.png create mode 100755 .github/sync-to-mirrors.sh create mode 100644 .github/workflows/audit-javascript.yml create mode 100644 .github/workflows/audit-rust.yml create mode 100644 .github/workflows/check-change-files.yml create mode 100644 .github/workflows/check-generated-files.yml create mode 100644 .github/workflows/check-license-header.yml create mode 100644 .github/workflows/covector-comment-on-fork.yml create mode 100644 .github/workflows/covector-status.yml create mode 100644 .github/workflows/covector-version-or-publish.yml create mode 100644 .github/workflows/fmt.yml create mode 100644 .github/workflows/integration-tests.yml create mode 100644 .github/workflows/lint-javascript.yml create mode 100644 .github/workflows/lint-rust.yml create mode 100644 .github/workflows/sync.yml create mode 100644 .github/workflows/test-android.yml create mode 100644 .github/workflows/test-api-e2e-mobile.yml create mode 100644 .github/workflows/test-api-e2e.yml create mode 100644 .github/workflows/test-rust.yml create mode 100644 .gitignore create mode 100644 .npmrc create mode 100644 .prettierignore create mode 100644 .prettierrc create mode 100644 .scripts/ci/check-change-files.js create mode 100644 .scripts/ci/check-license-header.js create mode 100755 .scripts/ci/collect-macos-crash-reports.sh create mode 100755 .scripts/ci/has-diff.sh create mode 100644 .taurignore create mode 100644 .vscode/extensions.json create mode 100644 Cargo.toml create mode 100644 LICENSE.spdx create mode 100644 LICENSE_APACHE-2.0 create mode 100644 LICENSE_MIT create mode 100644 README.md create mode 100644 SECURITY.md create mode 100644 eslint.config.js create mode 100644 examples/api/.setup-cross.sh create mode 100644 examples/api/.taurignore create mode 100644 examples/api/CHANGELOG.md create mode 100644 examples/api/README.md create mode 100644 examples/api/index.html create mode 100644 examples/api/isolation-dist/index.html create mode 100644 examples/api/isolation-dist/index.js create mode 100644 examples/api/package.json create mode 100644 examples/api/public/tauri_logo.png create mode 100644 examples/api/src-tauri/.gitignore create mode 100644 examples/api/src-tauri/.taurignore create mode 100644 examples/api/src-tauri/CHANGELOG.md create mode 100644 examples/api/src-tauri/Cargo.toml create mode 100644 examples/api/src-tauri/Cross.toml create mode 100644 examples/api/src-tauri/Info.plist create mode 100644 examples/api/src-tauri/build.rs create mode 100644 examples/api/src-tauri/capabilities/base.json create mode 100644 examples/api/src-tauri/capabilities/desktop.json create mode 100644 examples/api/src-tauri/capabilities/mobile.json create mode 100644 examples/api/src-tauri/gen/android/.editorconfig create mode 100644 examples/api/src-tauri/gen/android/.gitignore create mode 100644 examples/api/src-tauri/gen/android/app/.gitignore create mode 100644 examples/api/src-tauri/gen/android/app/build.gradle.kts create mode 100644 examples/api/src-tauri/gen/android/app/proguard-rules.pro create mode 100644 examples/api/src-tauri/gen/android/app/src/main/AndroidManifest.xml create mode 100644 examples/api/src-tauri/gen/android/app/src/main/java/com/tauri/api/MainActivity.kt create mode 100644 examples/api/src-tauri/gen/android/app/src/main/res/drawable-v24/ic_launcher_foreground.xml create mode 100644 examples/api/src-tauri/gen/android/app/src/main/res/drawable/ic_launcher_background.xml create mode 100644 examples/api/src-tauri/gen/android/app/src/main/res/layout/activity_main.xml create mode 100644 examples/api/src-tauri/gen/android/app/src/main/res/mipmap-hdpi/ic_launcher.png create mode 100644 examples/api/src-tauri/gen/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png create mode 100644 examples/api/src-tauri/gen/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png create mode 100644 examples/api/src-tauri/gen/android/app/src/main/res/mipmap-mdpi/ic_launcher.png create mode 100644 examples/api/src-tauri/gen/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png create mode 100644 examples/api/src-tauri/gen/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png create mode 100644 examples/api/src-tauri/gen/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png create mode 100644 examples/api/src-tauri/gen/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png create mode 100644 examples/api/src-tauri/gen/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png create mode 100644 examples/api/src-tauri/gen/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png create mode 100644 examples/api/src-tauri/gen/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png create mode 100644 examples/api/src-tauri/gen/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png create mode 100644 examples/api/src-tauri/gen/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png create mode 100644 examples/api/src-tauri/gen/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png create mode 100644 examples/api/src-tauri/gen/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png create mode 100644 examples/api/src-tauri/gen/android/app/src/main/res/values-night/themes.xml create mode 100644 examples/api/src-tauri/gen/android/app/src/main/res/values/colors.xml create mode 100644 examples/api/src-tauri/gen/android/app/src/main/res/values/strings.xml create mode 100644 examples/api/src-tauri/gen/android/app/src/main/res/values/themes.xml create mode 100644 examples/api/src-tauri/gen/android/app/src/main/res/xml/file_paths.xml create mode 100644 examples/api/src-tauri/gen/android/build.gradle.kts create mode 100644 examples/api/src-tauri/gen/android/buildSrc/build.gradle.kts create mode 100644 examples/api/src-tauri/gen/android/buildSrc/src/main/java/com/tauri/api/kotlin/BuildTask.kt create mode 100644 examples/api/src-tauri/gen/android/buildSrc/src/main/java/com/tauri/api/kotlin/RustPlugin.kt create mode 100644 examples/api/src-tauri/gen/android/gradle.properties create mode 100644 examples/api/src-tauri/gen/android/gradle/wrapper/gradle-wrapper.jar create mode 100644 examples/api/src-tauri/gen/android/gradle/wrapper/gradle-wrapper.properties create mode 100755 examples/api/src-tauri/gen/android/gradlew create mode 100644 examples/api/src-tauri/gen/android/gradlew.bat create mode 100644 examples/api/src-tauri/gen/android/settings.gradle create mode 100644 examples/api/src-tauri/gen/apple/.gitignore create mode 100644 examples/api/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-20x20@1x.png create mode 100644 examples/api/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-20x20@2x-1.png create mode 100644 examples/api/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-20x20@2x.png create mode 100644 examples/api/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-20x20@3x.png create mode 100644 examples/api/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-29x29@1x.png create mode 100644 examples/api/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-29x29@2x-1.png create mode 100644 examples/api/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-29x29@2x.png create mode 100644 examples/api/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-29x29@3x.png create mode 100644 examples/api/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-40x40@1x.png create mode 100644 examples/api/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-40x40@2x-1.png create mode 100644 examples/api/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-40x40@2x.png create mode 100644 examples/api/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-40x40@3x.png create mode 100644 examples/api/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-512@2x.png create mode 100644 examples/api/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-60x60@2x.png create mode 100644 examples/api/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-60x60@3x.png create mode 100644 examples/api/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-76x76@1x.png create mode 100644 examples/api/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-76x76@2x.png create mode 100644 examples/api/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-83.5x83.5@2x.png create mode 100644 examples/api/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/Contents.json create mode 100644 examples/api/src-tauri/gen/apple/Assets.xcassets/Contents.json create mode 100644 examples/api/src-tauri/gen/apple/ExportOptions.plist create mode 100644 examples/api/src-tauri/gen/apple/LaunchScreen.storyboard create mode 100644 examples/api/src-tauri/gen/apple/Podfile create mode 100644 examples/api/src-tauri/gen/apple/Sources/api/bindings/bindings.h create mode 100644 examples/api/src-tauri/gen/apple/Sources/api/main.mm create mode 100644 examples/api/src-tauri/gen/apple/api.xcodeproj/project.pbxproj create mode 100644 examples/api/src-tauri/gen/apple/api.xcodeproj/project.xcworkspace/contents.xcworkspacedata create mode 100644 examples/api/src-tauri/gen/apple/api.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings create mode 100644 examples/api/src-tauri/gen/apple/api.xcodeproj/xcshareddata/xcschemes/api_iOS.xcscheme create mode 100644 examples/api/src-tauri/gen/apple/api_iOS/Info.plist create mode 100644 examples/api/src-tauri/gen/apple/api_iOS/api_iOS.entitlements create mode 100644 examples/api/src-tauri/gen/apple/project.yml create mode 100644 examples/api/src-tauri/icons/128x128.png create mode 100644 examples/api/src-tauri/icons/128x128@2x.png create mode 100644 examples/api/src-tauri/icons/32x32.png create mode 100644 examples/api/src-tauri/icons/icon.ico create mode 100644 examples/api/src-tauri/icons/icon.png create mode 100644 examples/api/src-tauri/icons/tray_icon.png create mode 100644 examples/api/src-tauri/icons/tray_icon_with_transparency.ico create mode 100644 examples/api/src-tauri/icons/tray_icon_with_transparency.png create mode 100644 examples/api/src-tauri/locales/pt-BR.wxl create mode 100644 examples/api/src-tauri/src/cmd.rs create mode 100644 examples/api/src-tauri/src/lib.rs create mode 100644 examples/api/src-tauri/src/main.rs create mode 100644 examples/api/src-tauri/src/tray.rs create mode 100644 examples/api/src-tauri/tauri.conf.json create mode 100644 examples/api/src/App.svelte create mode 100644 examples/api/src/app.css create mode 100644 examples/api/src/lib/TitleBar.svelte create mode 100644 examples/api/src/lib/utils.js create mode 100644 examples/api/src/main.ts create mode 100644 examples/api/src/views/Autostart.svelte create mode 100644 examples/api/src/views/Biometric.svelte create mode 100644 examples/api/src/views/Cli.svelte create mode 100644 examples/api/src/views/Clipboard.svelte create mode 100644 examples/api/src/views/Communication.svelte create mode 100644 examples/api/src/views/DeepLink.svelte create mode 100644 examples/api/src/views/Dialog.svelte create mode 100644 examples/api/src/views/FileSystem.svelte create mode 100644 examples/api/src/views/Geolocation.svelte create mode 100644 examples/api/src/views/Haptics.svelte create mode 100644 examples/api/src/views/Http.svelte create mode 100644 examples/api/src/views/Nfc.svelte create mode 100644 examples/api/src/views/Notifications.svelte create mode 100644 examples/api/src/views/Opener.svelte create mode 100644 examples/api/src/views/Positioner.svelte create mode 100644 examples/api/src/views/Scanner.svelte create mode 100644 examples/api/src/views/Shell.svelte create mode 100644 examples/api/src/views/Shortcuts.svelte create mode 100644 examples/api/src/views/Sql.svelte create mode 100644 examples/api/src/views/Store.svelte create mode 100644 examples/api/src/views/Stronghold.svelte create mode 100644 examples/api/src/views/Updater.svelte create mode 100644 examples/api/src/views/Upload.svelte create mode 100644 examples/api/src/views/WebRTC.svelte create mode 100644 examples/api/src/views/WebSocket.svelte create mode 100644 examples/api/src/views/Welcome.svelte create mode 100644 examples/api/tsconfig.json create mode 100644 examples/api/unocss.config.js create mode 100644 examples/api/updater-test/.gitignore create mode 100644 examples/api/updater-test/README.md create mode 100644 examples/api/updater-test/updater.json create mode 100644 examples/api/vite.config.ts create mode 100644 package.json create mode 100644 packages/api-e2e/README.md create mode 100644 packages/api-e2e/package.json create mode 100644 packages/api-e2e/tauri.e2e.conf.json create mode 100644 packages/api-e2e/test/helpers/index.ts create mode 100644 packages/api-e2e/test/helpers/server.ts create mode 100644 packages/api-e2e/test/specs/app.spec.ts create mode 100644 packages/api-e2e/test/specs/autostart.spec.ts create mode 100644 packages/api-e2e/test/specs/cli.spec.ts create mode 100644 packages/api-e2e/test/specs/clipboard-manager.spec.ts create mode 100644 packages/api-e2e/test/specs/deep-link.spec.ts create mode 100644 packages/api-e2e/test/specs/fs.spec.ts create mode 100644 packages/api-e2e/test/specs/global-shortcut.spec.ts create mode 100644 packages/api-e2e/test/specs/http.spec.ts create mode 100644 packages/api-e2e/test/specs/log.spec.ts create mode 100644 packages/api-e2e/test/specs/notification.spec.ts create mode 100644 packages/api-e2e/test/specs/opener.spec.ts create mode 100644 packages/api-e2e/test/specs/os.spec.ts create mode 100644 packages/api-e2e/test/specs/plugins.spec.ts create mode 100644 packages/api-e2e/test/specs/positioner.spec.ts create mode 100644 packages/api-e2e/test/specs/shell.spec.ts create mode 100644 packages/api-e2e/test/specs/sql.spec.ts create mode 100644 packages/api-e2e/test/specs/store.spec.ts create mode 100644 packages/api-e2e/test/specs/stronghold.spec.ts create mode 100644 packages/api-e2e/test/specs/updater.spec.ts create mode 100644 packages/api-e2e/test/specs/upload.spec.ts create mode 100644 packages/api-e2e/test/specs/websocket.spec.ts create mode 100644 packages/api-e2e/test/specs/window-state.spec.ts create mode 100644 packages/api-e2e/test/types/global.d.ts create mode 100644 packages/api-e2e/tsconfig.json create mode 100644 packages/api-e2e/wdio.android.conf.ts create mode 100644 packages/api-e2e/wdio.conf.ts create mode 100644 packages/api-e2e/wdio.ios.conf.ts create mode 100644 packages/api-e2e/wdio.mobile.ts create mode 100644 plugins/autostart/CHANGELOG.md create mode 100644 plugins/autostart/Cargo.toml create mode 100644 plugins/autostart/LICENSE.spdx create mode 100644 plugins/autostart/LICENSE_APACHE-2.0 create mode 100644 plugins/autostart/LICENSE_MIT create mode 100644 plugins/autostart/README.md create mode 100644 plugins/autostart/SECURITY.md create mode 100644 plugins/autostart/api-iife.js create mode 100644 plugins/autostart/banner.png create mode 100644 plugins/autostart/build.rs create mode 100644 plugins/autostart/guest-js/index.ts create mode 100644 plugins/autostart/package.json create mode 100644 plugins/autostart/permissions/autogenerated/commands/disable.toml create mode 100644 plugins/autostart/permissions/autogenerated/commands/enable.toml create mode 100644 plugins/autostart/permissions/autogenerated/commands/is_enabled.toml create mode 100644 plugins/autostart/permissions/autogenerated/reference.md create mode 100644 plugins/autostart/permissions/default.toml create mode 100644 plugins/autostart/permissions/schemas/schema.json create mode 100644 plugins/autostart/rollup.config.js create mode 100644 plugins/autostart/src/lib.rs create mode 100644 plugins/autostart/tsconfig.json create mode 100644 plugins/barcode-scanner/CHANGELOG.md create mode 100644 plugins/barcode-scanner/Cargo.toml create mode 100644 plugins/barcode-scanner/LICENSE.spdx create mode 100644 plugins/barcode-scanner/LICENSE_APACHE-2.0 create mode 100644 plugins/barcode-scanner/LICENSE_MIT create mode 100644 plugins/barcode-scanner/README.md create mode 100644 plugins/barcode-scanner/SECURITY.md create mode 100644 plugins/barcode-scanner/android/.gitignore create mode 100644 plugins/barcode-scanner/android/build.gradle.kts create mode 100644 plugins/barcode-scanner/android/consumer-rules.pro create mode 100644 plugins/barcode-scanner/android/settings.gradle create mode 100644 plugins/barcode-scanner/android/src/androidTest/java/ExampleInstrumentedTest.kt create mode 100644 plugins/barcode-scanner/android/src/main/AndroidManifest.xml create mode 100644 plugins/barcode-scanner/android/src/main/java/BarcodeScannerPlugin.kt create mode 100644 plugins/barcode-scanner/android/src/main/java/GraphicOverlay.kt create mode 100644 plugins/barcode-scanner/android/src/test/java/ExampleUnitTest.kt create mode 100644 plugins/barcode-scanner/api-iife.js create mode 100644 plugins/barcode-scanner/banner.png create mode 100644 plugins/barcode-scanner/build.rs create mode 100644 plugins/barcode-scanner/contributors/crabnebula.svg create mode 100644 plugins/barcode-scanner/contributors/impierce.svg create mode 100644 plugins/barcode-scanner/guest-js/index.ts create mode 100644 plugins/barcode-scanner/ios/.gitignore create mode 100644 plugins/barcode-scanner/ios/Package.swift create mode 100644 plugins/barcode-scanner/ios/README.md create mode 100644 plugins/barcode-scanner/ios/Sources/BarcodeScannerPlugin.swift create mode 100644 plugins/barcode-scanner/ios/Sources/CameraView.swift create mode 100644 plugins/barcode-scanner/ios/Sources/Utils.swift create mode 100644 plugins/barcode-scanner/ios/Tests/PluginTests/PluginTests.swift create mode 100644 plugins/barcode-scanner/package.json create mode 100644 plugins/barcode-scanner/permissions/autogenerated/commands/cancel.toml create mode 100644 plugins/barcode-scanner/permissions/autogenerated/commands/check_permissions.toml create mode 100644 plugins/barcode-scanner/permissions/autogenerated/commands/open_app_settings.toml create mode 100644 plugins/barcode-scanner/permissions/autogenerated/commands/request_permissions.toml create mode 100644 plugins/barcode-scanner/permissions/autogenerated/commands/scan.toml create mode 100644 plugins/barcode-scanner/permissions/autogenerated/commands/vibrate.toml create mode 100644 plugins/barcode-scanner/permissions/autogenerated/reference.md create mode 100644 plugins/barcode-scanner/permissions/default.toml create mode 100644 plugins/barcode-scanner/permissions/schemas/schema.json create mode 100644 plugins/barcode-scanner/rollup.config.js create mode 100644 plugins/barcode-scanner/src/error.rs create mode 100644 plugins/barcode-scanner/src/lib.rs create mode 100644 plugins/barcode-scanner/src/mobile.rs create mode 100644 plugins/barcode-scanner/src/models.rs create mode 100644 plugins/barcode-scanner/tsconfig.json create mode 100644 plugins/biometric/CHANGELOG.md create mode 100644 plugins/biometric/Cargo.toml create mode 100644 plugins/biometric/LICENSE.spdx create mode 100644 plugins/biometric/LICENSE_APACHE-2.0 create mode 100644 plugins/biometric/LICENSE_MIT create mode 100644 plugins/biometric/README.md create mode 100644 plugins/biometric/SECURITY.md create mode 100644 plugins/biometric/android/.gitignore create mode 100644 plugins/biometric/android/build.gradle.kts create mode 100644 plugins/biometric/android/consumer-rules.pro create mode 100644 plugins/biometric/android/settings.gradle create mode 100644 plugins/biometric/android/src/androidTest/java/ExampleInstrumentedTest.kt create mode 100644 plugins/biometric/android/src/main/AndroidManifest.xml create mode 100644 plugins/biometric/android/src/main/java/BiometricActivity.kt create mode 100644 plugins/biometric/android/src/main/java/BiometricPlugin.kt create mode 100644 plugins/biometric/android/src/main/res/layout/auth_activity.xml create mode 100644 plugins/biometric/android/src/main/res/values/styles.xml create mode 100644 plugins/biometric/android/src/test/java/ExampleUnitTest.kt create mode 100644 plugins/biometric/api-iife.js create mode 100644 plugins/biometric/build.rs create mode 100644 plugins/biometric/contributors/crabnebula.svg create mode 100644 plugins/biometric/contributors/impierce.svg create mode 100644 plugins/biometric/guest-js/index.ts create mode 100644 plugins/biometric/ios/.gitignore create mode 100644 plugins/biometric/ios/Package.swift create mode 100644 plugins/biometric/ios/README.md create mode 100644 plugins/biometric/ios/Sources/BiometricPlugin.swift create mode 100644 plugins/biometric/ios/Tests/PluginTests/PluginTests.swift create mode 100644 plugins/biometric/package.json create mode 100644 plugins/biometric/permissions/autogenerated/commands/authenticate.toml create mode 100644 plugins/biometric/permissions/autogenerated/commands/status.toml create mode 100644 plugins/biometric/permissions/autogenerated/reference.md create mode 100644 plugins/biometric/permissions/default.toml create mode 100644 plugins/biometric/permissions/schemas/schema.json create mode 100644 plugins/biometric/rollup.config.js create mode 100644 plugins/biometric/src/error.rs create mode 100644 plugins/biometric/src/lib.rs create mode 100644 plugins/biometric/src/models.rs create mode 100644 plugins/biometric/tsconfig.json create mode 100644 plugins/cli/CHANGELOG.md create mode 100644 plugins/cli/Cargo.toml create mode 100644 plugins/cli/LICENSE.spdx create mode 100644 plugins/cli/LICENSE_APACHE-2.0 create mode 100644 plugins/cli/LICENSE_MIT create mode 100644 plugins/cli/README.md create mode 100644 plugins/cli/SECURITY.md create mode 100644 plugins/cli/api-iife.js create mode 100644 plugins/cli/banner.png create mode 100644 plugins/cli/build.rs create mode 100644 plugins/cli/guest-js/index.ts create mode 100644 plugins/cli/package.json create mode 100644 plugins/cli/permissions/autogenerated/commands/cli_matches.toml create mode 100644 plugins/cli/permissions/autogenerated/reference.md create mode 100644 plugins/cli/permissions/default.toml create mode 100644 plugins/cli/permissions/schemas/schema.json create mode 100644 plugins/cli/rollup.config.js create mode 100644 plugins/cli/src/config.rs create mode 100644 plugins/cli/src/error.rs create mode 100644 plugins/cli/src/lib.rs create mode 100644 plugins/cli/src/parser.rs create mode 100644 plugins/cli/src/parser/macros.rs create mode 100644 plugins/cli/tsconfig.json create mode 100644 plugins/clipboard-manager/CHANGELOG.md create mode 100644 plugins/clipboard-manager/Cargo.toml create mode 100644 plugins/clipboard-manager/LICENSE.spdx create mode 100644 plugins/clipboard-manager/LICENSE_APACHE-2.0 create mode 100644 plugins/clipboard-manager/LICENSE_MIT create mode 100644 plugins/clipboard-manager/README.md create mode 100644 plugins/clipboard-manager/SECURITY.md create mode 100644 plugins/clipboard-manager/android/.gitignore create mode 100644 plugins/clipboard-manager/android/build.gradle.kts create mode 100644 plugins/clipboard-manager/android/consumer-rules.pro create mode 100644 plugins/clipboard-manager/android/settings.gradle create mode 100644 plugins/clipboard-manager/android/src/androidTest/java/ExampleInstrumentedTest.kt create mode 100644 plugins/clipboard-manager/android/src/main/AndroidManifest.xml create mode 100644 plugins/clipboard-manager/android/src/main/java/ClipboardPlugin.kt create mode 100644 plugins/clipboard-manager/android/src/test/java/ExampleUnitTest.kt create mode 100644 plugins/clipboard-manager/api-iife.js create mode 100644 plugins/clipboard-manager/banner.png create mode 100644 plugins/clipboard-manager/build.rs create mode 100644 plugins/clipboard-manager/guest-js/index.ts create mode 100644 plugins/clipboard-manager/ios/.gitignore create mode 100644 plugins/clipboard-manager/ios/Package.swift create mode 100644 plugins/clipboard-manager/ios/README.md create mode 100644 plugins/clipboard-manager/ios/Sources/ClipboardPlugin.swift create mode 100644 plugins/clipboard-manager/ios/Tests/PluginTests/PluginTests.swift create mode 100644 plugins/clipboard-manager/package.json create mode 100644 plugins/clipboard-manager/permissions/autogenerated/commands/clear.toml create mode 100644 plugins/clipboard-manager/permissions/autogenerated/commands/read_image.toml create mode 100644 plugins/clipboard-manager/permissions/autogenerated/commands/read_text.toml create mode 100644 plugins/clipboard-manager/permissions/autogenerated/commands/write_html.toml create mode 100644 plugins/clipboard-manager/permissions/autogenerated/commands/write_image.toml create mode 100644 plugins/clipboard-manager/permissions/autogenerated/commands/write_text.toml create mode 100644 plugins/clipboard-manager/permissions/autogenerated/reference.md create mode 100644 plugins/clipboard-manager/permissions/default.toml create mode 100644 plugins/clipboard-manager/permissions/schemas/schema.json create mode 100644 plugins/clipboard-manager/rollup.config.js create mode 100644 plugins/clipboard-manager/src/commands.rs create mode 100644 plugins/clipboard-manager/src/desktop.rs create mode 100644 plugins/clipboard-manager/src/error.rs create mode 100644 plugins/clipboard-manager/src/lib.rs create mode 100644 plugins/clipboard-manager/src/mobile.rs create mode 100644 plugins/clipboard-manager/tsconfig.json create mode 100644 plugins/deep-link/.test-server/.well-known/apple-app-site-association create mode 100644 plugins/deep-link/.test-server/server.js create mode 100644 plugins/deep-link/CHANGELOG.md create mode 100644 plugins/deep-link/Cargo.toml create mode 100644 plugins/deep-link/LICENSE.spdx create mode 100644 plugins/deep-link/LICENSE_APACHE-2.0 create mode 100644 plugins/deep-link/LICENSE_MIT create mode 100644 plugins/deep-link/README.md create mode 100644 plugins/deep-link/SECURITY.md create mode 100644 plugins/deep-link/android/.gitignore create mode 100644 plugins/deep-link/android/build.gradle.kts create mode 100644 plugins/deep-link/android/consumer-rules.pro create mode 100644 plugins/deep-link/android/settings.gradle create mode 100644 plugins/deep-link/android/src/androidTest/java/ExampleInstrumentedTest.kt create mode 100644 plugins/deep-link/android/src/main/AndroidManifest.xml create mode 100644 plugins/deep-link/android/src/main/java/DeepLinkPlugin.kt create mode 100644 plugins/deep-link/android/src/test/java/ExampleUnitTest.kt create mode 100644 plugins/deep-link/api-iife.js create mode 100644 plugins/deep-link/banner.png create mode 100644 plugins/deep-link/build.rs create mode 100644 plugins/deep-link/contributors/crabnebula.svg create mode 100644 plugins/deep-link/contributors/impierce.svg create mode 100644 plugins/deep-link/examples/app/.gitignore create mode 100644 plugins/deep-link/examples/app/CHANGELOG.md create mode 100644 plugins/deep-link/examples/app/README.md create mode 100644 plugins/deep-link/examples/app/index.html create mode 100644 plugins/deep-link/examples/app/package.json create mode 100644 plugins/deep-link/examples/app/public/svelte.svg create mode 100644 plugins/deep-link/examples/app/public/tauri.svg create mode 100644 plugins/deep-link/examples/app/public/vite.svg create mode 100644 plugins/deep-link/examples/app/src-tauri/.gitignore create mode 100644 plugins/deep-link/examples/app/src-tauri/Cargo.toml create mode 100644 plugins/deep-link/examples/app/src-tauri/apple-app-site-association create mode 100644 plugins/deep-link/examples/app/src-tauri/assetlinks.json.txt create mode 100644 plugins/deep-link/examples/app/src-tauri/build.rs create mode 100644 plugins/deep-link/examples/app/src-tauri/capabilities/app.json create mode 100644 plugins/deep-link/examples/app/src-tauri/gen/android/.editorconfig create mode 100644 plugins/deep-link/examples/app/src-tauri/gen/android/.gitignore create mode 100644 plugins/deep-link/examples/app/src-tauri/gen/android/app/.gitignore create mode 100644 plugins/deep-link/examples/app/src-tauri/gen/android/app/build.gradle.kts create mode 100644 plugins/deep-link/examples/app/src-tauri/gen/android/app/proguard-rules.pro create mode 100644 plugins/deep-link/examples/app/src-tauri/gen/android/app/src/main/AndroidManifest.xml create mode 100644 plugins/deep-link/examples/app/src-tauri/gen/android/app/src/main/java/com/tauri/deep_link_example/MainActivity.kt create mode 100644 plugins/deep-link/examples/app/src-tauri/gen/android/app/src/main/res/drawable-v24/ic_launcher_foreground.xml create mode 100644 plugins/deep-link/examples/app/src-tauri/gen/android/app/src/main/res/drawable/ic_launcher_background.xml create mode 100644 plugins/deep-link/examples/app/src-tauri/gen/android/app/src/main/res/layout/activity_main.xml create mode 100644 plugins/deep-link/examples/app/src-tauri/gen/android/app/src/main/res/mipmap-hdpi/ic_launcher.png create mode 100644 plugins/deep-link/examples/app/src-tauri/gen/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png create mode 100644 plugins/deep-link/examples/app/src-tauri/gen/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png create mode 100644 plugins/deep-link/examples/app/src-tauri/gen/android/app/src/main/res/mipmap-mdpi/ic_launcher.png create mode 100644 plugins/deep-link/examples/app/src-tauri/gen/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png create mode 100644 plugins/deep-link/examples/app/src-tauri/gen/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png create mode 100644 plugins/deep-link/examples/app/src-tauri/gen/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png create mode 100644 plugins/deep-link/examples/app/src-tauri/gen/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png create mode 100644 plugins/deep-link/examples/app/src-tauri/gen/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png create mode 100644 plugins/deep-link/examples/app/src-tauri/gen/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png create mode 100644 plugins/deep-link/examples/app/src-tauri/gen/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png create mode 100644 plugins/deep-link/examples/app/src-tauri/gen/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png create mode 100644 plugins/deep-link/examples/app/src-tauri/gen/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png create mode 100644 plugins/deep-link/examples/app/src-tauri/gen/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png create mode 100644 plugins/deep-link/examples/app/src-tauri/gen/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png create mode 100644 plugins/deep-link/examples/app/src-tauri/gen/android/app/src/main/res/values-night/themes.xml create mode 100644 plugins/deep-link/examples/app/src-tauri/gen/android/app/src/main/res/values/colors.xml create mode 100644 plugins/deep-link/examples/app/src-tauri/gen/android/app/src/main/res/values/strings.xml create mode 100644 plugins/deep-link/examples/app/src-tauri/gen/android/app/src/main/res/values/themes.xml create mode 100644 plugins/deep-link/examples/app/src-tauri/gen/android/app/src/main/res/xml/file_paths.xml create mode 100644 plugins/deep-link/examples/app/src-tauri/gen/android/build.gradle.kts create mode 100644 plugins/deep-link/examples/app/src-tauri/gen/android/buildSrc/build.gradle.kts create mode 100644 plugins/deep-link/examples/app/src-tauri/gen/android/buildSrc/src/main/java/com/tauri/deep_link_example/kotlin/BuildTask.kt create mode 100644 plugins/deep-link/examples/app/src-tauri/gen/android/buildSrc/src/main/java/com/tauri/deep_link_example/kotlin/RustPlugin.kt create mode 100644 plugins/deep-link/examples/app/src-tauri/gen/android/gradle.properties create mode 100644 plugins/deep-link/examples/app/src-tauri/gen/android/gradle/wrapper/gradle-wrapper.jar create mode 100644 plugins/deep-link/examples/app/src-tauri/gen/android/gradle/wrapper/gradle-wrapper.properties create mode 100755 plugins/deep-link/examples/app/src-tauri/gen/android/gradlew create mode 100644 plugins/deep-link/examples/app/src-tauri/gen/android/gradlew.bat create mode 100644 plugins/deep-link/examples/app/src-tauri/gen/android/settings.gradle create mode 100644 plugins/deep-link/examples/app/src-tauri/gen/apple/.gitignore create mode 100644 plugins/deep-link/examples/app/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-20x20@1x.png create mode 100644 plugins/deep-link/examples/app/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-20x20@2x-1.png create mode 100644 plugins/deep-link/examples/app/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-20x20@2x.png create mode 100644 plugins/deep-link/examples/app/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-20x20@3x.png create mode 100644 plugins/deep-link/examples/app/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-29x29@1x.png create mode 100644 plugins/deep-link/examples/app/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-29x29@2x-1.png create mode 100644 plugins/deep-link/examples/app/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-29x29@2x.png create mode 100644 plugins/deep-link/examples/app/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-29x29@3x.png create mode 100644 plugins/deep-link/examples/app/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-40x40@1x.png create mode 100644 plugins/deep-link/examples/app/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-40x40@2x-1.png create mode 100644 plugins/deep-link/examples/app/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-40x40@2x.png create mode 100644 plugins/deep-link/examples/app/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-40x40@3x.png create mode 100644 plugins/deep-link/examples/app/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-512@2x.png create mode 100644 plugins/deep-link/examples/app/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-60x60@2x.png create mode 100644 plugins/deep-link/examples/app/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-60x60@3x.png create mode 100644 plugins/deep-link/examples/app/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-76x76@1x.png create mode 100644 plugins/deep-link/examples/app/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-76x76@2x.png create mode 100644 plugins/deep-link/examples/app/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-83.5x83.5@2x.png create mode 100644 plugins/deep-link/examples/app/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/Contents.json create mode 100644 plugins/deep-link/examples/app/src-tauri/gen/apple/Assets.xcassets/Contents.json create mode 100644 plugins/deep-link/examples/app/src-tauri/gen/apple/ExportOptions.plist create mode 100644 plugins/deep-link/examples/app/src-tauri/gen/apple/LaunchScreen.storyboard create mode 100644 plugins/deep-link/examples/app/src-tauri/gen/apple/Podfile create mode 100644 plugins/deep-link/examples/app/src-tauri/gen/apple/Sources/deep-link-example/bindings/bindings.h create mode 100644 plugins/deep-link/examples/app/src-tauri/gen/apple/Sources/deep-link-example/main.mm create mode 100644 plugins/deep-link/examples/app/src-tauri/gen/apple/deep-link-example.xcodeproj/project.pbxproj create mode 100644 plugins/deep-link/examples/app/src-tauri/gen/apple/deep-link-example.xcodeproj/project.xcworkspace/contents.xcworkspacedata create mode 100644 plugins/deep-link/examples/app/src-tauri/gen/apple/deep-link-example.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings create mode 100644 plugins/deep-link/examples/app/src-tauri/gen/apple/deep-link-example.xcodeproj/xcshareddata/xcschemes/deep-link-example_iOS.xcscheme create mode 100644 plugins/deep-link/examples/app/src-tauri/gen/apple/deep-link-example_iOS/Info.plist create mode 100644 plugins/deep-link/examples/app/src-tauri/gen/apple/deep-link-example_iOS/deep-link-example_iOS.entitlements create mode 100644 plugins/deep-link/examples/app/src-tauri/gen/apple/project.yml create mode 100644 plugins/deep-link/examples/app/src-tauri/icons/128x128.png create mode 100644 plugins/deep-link/examples/app/src-tauri/icons/128x128@2x.png create mode 100644 plugins/deep-link/examples/app/src-tauri/icons/32x32.png create mode 100644 plugins/deep-link/examples/app/src-tauri/icons/icon.ico create mode 100644 plugins/deep-link/examples/app/src-tauri/icons/icon.png create mode 100644 plugins/deep-link/examples/app/src-tauri/server.js create mode 100644 plugins/deep-link/examples/app/src-tauri/src/lib.rs create mode 100644 plugins/deep-link/examples/app/src-tauri/src/main.rs create mode 100644 plugins/deep-link/examples/app/src-tauri/tauri.conf.json create mode 100644 plugins/deep-link/examples/app/src/assets/tauri.svg create mode 100644 plugins/deep-link/examples/app/src/assets/typescript.svg create mode 100644 plugins/deep-link/examples/app/src/assets/vite.svg create mode 100644 plugins/deep-link/examples/app/src/main.ts create mode 100644 plugins/deep-link/examples/app/src/styles.css create mode 100644 plugins/deep-link/examples/app/tsconfig.json create mode 100644 plugins/deep-link/examples/app/vite.config.ts create mode 100644 plugins/deep-link/guest-js/index.ts create mode 100644 plugins/deep-link/package.json create mode 100644 plugins/deep-link/permissions/autogenerated/commands/get_current.toml create mode 100644 plugins/deep-link/permissions/autogenerated/commands/is_registered.toml create mode 100644 plugins/deep-link/permissions/autogenerated/commands/register.toml create mode 100644 plugins/deep-link/permissions/autogenerated/commands/unregister.toml create mode 100644 plugins/deep-link/permissions/autogenerated/reference.md create mode 100644 plugins/deep-link/permissions/default.toml create mode 100644 plugins/deep-link/permissions/schemas/schema.json create mode 100644 plugins/deep-link/rollup.config.js create mode 100644 plugins/deep-link/src/commands.rs create mode 100644 plugins/deep-link/src/config.rs create mode 100644 plugins/deep-link/src/error.rs create mode 100644 plugins/deep-link/src/lib.rs create mode 100644 plugins/deep-link/src/template.desktop create mode 100644 plugins/deep-link/tsconfig.json create mode 100644 plugins/dialog/CHANGELOG.md create mode 100644 plugins/dialog/Cargo.toml create mode 100644 plugins/dialog/LICENSE.spdx create mode 100644 plugins/dialog/LICENSE_APACHE-2.0 create mode 100644 plugins/dialog/LICENSE_MIT create mode 100644 plugins/dialog/README.md create mode 100644 plugins/dialog/SECURITY.md create mode 100644 plugins/dialog/android/.gitignore create mode 100644 plugins/dialog/android/build.gradle.kts create mode 100644 plugins/dialog/android/consumer-rules.pro create mode 100644 plugins/dialog/android/settings.gradle create mode 100644 plugins/dialog/android/src/androidTest/java/ExampleInstrumentedTest.kt create mode 100644 plugins/dialog/android/src/main/AndroidManifest.xml create mode 100644 plugins/dialog/android/src/main/java/DialogPlugin.kt create mode 100644 plugins/dialog/android/src/main/java/FilePickerUtils.kt create mode 100644 plugins/dialog/android/src/test/java/ExampleUnitTest.kt create mode 100644 plugins/dialog/api-iife.js create mode 100644 plugins/dialog/banner.png create mode 100644 plugins/dialog/build.rs create mode 100644 plugins/dialog/guest-js/index.ts create mode 100644 plugins/dialog/guest-js/init.ts create mode 100644 plugins/dialog/ios/.gitignore create mode 100644 plugins/dialog/ios/Package.swift create mode 100644 plugins/dialog/ios/README.md create mode 100644 plugins/dialog/ios/Sources/DialogPlugin.swift create mode 100644 plugins/dialog/ios/Sources/FilePickerController.swift create mode 100644 plugins/dialog/ios/Tests/PluginTests/PluginTests.swift create mode 100644 plugins/dialog/package.json create mode 100644 plugins/dialog/permissions/ask.toml create mode 100644 plugins/dialog/permissions/autogenerated/commands/message.toml create mode 100644 plugins/dialog/permissions/autogenerated/commands/open.toml create mode 100644 plugins/dialog/permissions/autogenerated/commands/save.toml create mode 100644 plugins/dialog/permissions/autogenerated/reference.md create mode 100644 plugins/dialog/permissions/confirm.toml create mode 100644 plugins/dialog/permissions/default.toml create mode 100644 plugins/dialog/permissions/schemas/schema.json create mode 100644 plugins/dialog/rollup.config.js create mode 100644 plugins/dialog/src/commands.rs create mode 100644 plugins/dialog/src/desktop.rs create mode 100644 plugins/dialog/src/error.rs create mode 100644 plugins/dialog/src/init-iife.js create mode 100644 plugins/dialog/src/lib.rs create mode 100644 plugins/dialog/src/mobile.rs create mode 100644 plugins/dialog/src/models.rs create mode 100644 plugins/dialog/test/tauri.conf.json create mode 100644 plugins/dialog/tsconfig.json create mode 100644 plugins/fs/CHANGELOG.md create mode 100644 plugins/fs/Cargo.toml create mode 100644 plugins/fs/LICENSE.spdx create mode 100644 plugins/fs/LICENSE_APACHE-2.0 create mode 100644 plugins/fs/LICENSE_MIT create mode 100644 plugins/fs/README.md create mode 100644 plugins/fs/SECURITY.md create mode 100644 plugins/fs/android/.gitignore create mode 100644 plugins/fs/android/build.gradle.kts create mode 100644 plugins/fs/android/consumer-rules.pro create mode 100644 plugins/fs/android/settings.gradle create mode 100644 plugins/fs/android/src/androidTest/java/ExampleInstrumentedTest.kt create mode 100644 plugins/fs/android/src/main/AndroidManifest.xml create mode 100644 plugins/fs/android/src/main/java/FsPlugin.kt create mode 100644 plugins/fs/android/src/test/java/ExampleUnitTest.kt create mode 100644 plugins/fs/api-iife.js create mode 100644 plugins/fs/banner.png create mode 100644 plugins/fs/build.rs create mode 100644 plugins/fs/guest-js/index.ts create mode 100644 plugins/fs/package.json create mode 100644 plugins/fs/permissions/app.toml create mode 100644 plugins/fs/permissions/autogenerated/base-directories/appcache.toml create mode 100644 plugins/fs/permissions/autogenerated/base-directories/appconfig.toml create mode 100644 plugins/fs/permissions/autogenerated/base-directories/appdata.toml create mode 100644 plugins/fs/permissions/autogenerated/base-directories/applocaldata.toml create mode 100644 plugins/fs/permissions/autogenerated/base-directories/applog.toml create mode 100644 plugins/fs/permissions/autogenerated/base-directories/audio.toml create mode 100644 plugins/fs/permissions/autogenerated/base-directories/cache.toml create mode 100644 plugins/fs/permissions/autogenerated/base-directories/config.toml create mode 100644 plugins/fs/permissions/autogenerated/base-directories/data.toml create mode 100644 plugins/fs/permissions/autogenerated/base-directories/desktop.toml create mode 100644 plugins/fs/permissions/autogenerated/base-directories/document.toml create mode 100644 plugins/fs/permissions/autogenerated/base-directories/download.toml create mode 100644 plugins/fs/permissions/autogenerated/base-directories/exe.toml create mode 100644 plugins/fs/permissions/autogenerated/base-directories/font.toml create mode 100644 plugins/fs/permissions/autogenerated/base-directories/home.toml create mode 100644 plugins/fs/permissions/autogenerated/base-directories/localdata.toml create mode 100644 plugins/fs/permissions/autogenerated/base-directories/log.toml create mode 100644 plugins/fs/permissions/autogenerated/base-directories/picture.toml create mode 100644 plugins/fs/permissions/autogenerated/base-directories/public.toml create mode 100644 plugins/fs/permissions/autogenerated/base-directories/resource.toml create mode 100644 plugins/fs/permissions/autogenerated/base-directories/runtime.toml create mode 100644 plugins/fs/permissions/autogenerated/base-directories/temp.toml create mode 100644 plugins/fs/permissions/autogenerated/base-directories/template.toml create mode 100644 plugins/fs/permissions/autogenerated/base-directories/video.toml create mode 100644 plugins/fs/permissions/autogenerated/commands/copy_file.toml create mode 100644 plugins/fs/permissions/autogenerated/commands/create.toml create mode 100644 plugins/fs/permissions/autogenerated/commands/exists.toml create mode 100644 plugins/fs/permissions/autogenerated/commands/fstat.toml create mode 100644 plugins/fs/permissions/autogenerated/commands/ftruncate.toml create mode 100644 plugins/fs/permissions/autogenerated/commands/lstat.toml create mode 100644 plugins/fs/permissions/autogenerated/commands/mkdir.toml create mode 100644 plugins/fs/permissions/autogenerated/commands/open.toml create mode 100644 plugins/fs/permissions/autogenerated/commands/read.toml create mode 100644 plugins/fs/permissions/autogenerated/commands/read_dir.toml create mode 100644 plugins/fs/permissions/autogenerated/commands/read_file.toml create mode 100644 plugins/fs/permissions/autogenerated/commands/read_text_file.toml create mode 100644 plugins/fs/permissions/autogenerated/commands/read_text_file_lines.toml create mode 100644 plugins/fs/permissions/autogenerated/commands/read_text_file_lines_next.toml create mode 100644 plugins/fs/permissions/autogenerated/commands/remove.toml create mode 100644 plugins/fs/permissions/autogenerated/commands/rename.toml create mode 100644 plugins/fs/permissions/autogenerated/commands/seek.toml create mode 100644 plugins/fs/permissions/autogenerated/commands/size.toml create mode 100644 plugins/fs/permissions/autogenerated/commands/start_accessing_security_scoped_resource.toml create mode 100644 plugins/fs/permissions/autogenerated/commands/stat.toml create mode 100644 plugins/fs/permissions/autogenerated/commands/stop_accessing_security_scoped_resource.toml create mode 100644 plugins/fs/permissions/autogenerated/commands/truncate.toml create mode 100644 plugins/fs/permissions/autogenerated/commands/unwatch.toml create mode 100644 plugins/fs/permissions/autogenerated/commands/watch.toml create mode 100644 plugins/fs/permissions/autogenerated/commands/write.toml create mode 100644 plugins/fs/permissions/autogenerated/commands/write_file.toml create mode 100644 plugins/fs/permissions/autogenerated/commands/write_text_file.toml create mode 100644 plugins/fs/permissions/autogenerated/reference.md create mode 100644 plugins/fs/permissions/create-app-specific-dirs.toml create mode 100644 plugins/fs/permissions/default.toml create mode 100644 plugins/fs/permissions/deny-default.toml create mode 100644 plugins/fs/permissions/deny-webview-data.toml create mode 100644 plugins/fs/permissions/read-all.toml create mode 100644 plugins/fs/permissions/read-app-specific-dirs-recursive.toml create mode 100644 plugins/fs/permissions/read-dirs.toml create mode 100644 plugins/fs/permissions/read-files.toml create mode 100644 plugins/fs/permissions/read-meta.toml create mode 100644 plugins/fs/permissions/scope.toml create mode 100644 plugins/fs/permissions/write-all.toml create mode 100644 plugins/fs/permissions/write-files.toml create mode 100644 plugins/fs/rollup.config.js create mode 100644 plugins/fs/src/android.rs create mode 100644 plugins/fs/src/commands.rs create mode 100644 plugins/fs/src/config.rs create mode 100644 plugins/fs/src/desktop.rs create mode 100644 plugins/fs/src/error.rs create mode 100644 plugins/fs/src/file_path.rs create mode 100644 plugins/fs/src/ios.rs create mode 100644 plugins/fs/src/lib.rs create mode 100644 plugins/fs/src/models.rs create mode 100644 plugins/fs/src/scope.rs create mode 100644 plugins/fs/src/watcher.rs create mode 100644 plugins/fs/tsconfig.json create mode 100644 plugins/geolocation/CHANGELOG.md create mode 100644 plugins/geolocation/Cargo.toml create mode 100644 plugins/geolocation/LICENSE.spdx create mode 100644 plugins/geolocation/LICENSE_APACHE-2.0 create mode 100644 plugins/geolocation/LICENSE_MIT create mode 100644 plugins/geolocation/README.md create mode 100644 plugins/geolocation/SECURITY.md create mode 100644 plugins/geolocation/android/.gitignore create mode 100644 plugins/geolocation/android/build.gradle.kts create mode 100644 plugins/geolocation/android/consumer-rules.pro create mode 100644 plugins/geolocation/android/settings.gradle create mode 100644 plugins/geolocation/android/src/androidTest/java/ExampleInstrumentedTest.kt create mode 100644 plugins/geolocation/android/src/main/AndroidManifest.xml create mode 100644 plugins/geolocation/android/src/main/java/Geolocation.kt create mode 100644 plugins/geolocation/android/src/main/java/GeolocationPlugin.kt create mode 100644 plugins/geolocation/android/src/test/java/ExampleUnitTest.kt create mode 100644 plugins/geolocation/api-iife.js create mode 100644 plugins/geolocation/build.rs create mode 100644 plugins/geolocation/contributors/crabnebula.svg create mode 100644 plugins/geolocation/contributors/rescue.png create mode 100644 plugins/geolocation/guest-js/index.ts create mode 100644 plugins/geolocation/ios/.gitignore create mode 100644 plugins/geolocation/ios/Package.swift create mode 100644 plugins/geolocation/ios/README.md create mode 100644 plugins/geolocation/ios/Sources/GeolocationPlugin.swift create mode 100644 plugins/geolocation/ios/Tests/PluginTests/PluginTests.swift create mode 100644 plugins/geolocation/package.json create mode 100644 plugins/geolocation/permissions/autogenerated/commands/check_permissions.toml create mode 100644 plugins/geolocation/permissions/autogenerated/commands/clear_permissions.toml create mode 100644 plugins/geolocation/permissions/autogenerated/commands/clear_watch.toml create mode 100644 plugins/geolocation/permissions/autogenerated/commands/get_current_position.toml create mode 100644 plugins/geolocation/permissions/autogenerated/commands/request_permissions.toml create mode 100644 plugins/geolocation/permissions/autogenerated/commands/watch_position.toml create mode 100644 plugins/geolocation/permissions/autogenerated/reference.md create mode 100644 plugins/geolocation/permissions/schemas/schema.json create mode 100644 plugins/geolocation/rollup.config.js create mode 100644 plugins/geolocation/src/commands.rs create mode 100644 plugins/geolocation/src/desktop.rs create mode 100644 plugins/geolocation/src/error.rs create mode 100644 plugins/geolocation/src/lib.rs create mode 100644 plugins/geolocation/src/mobile.rs create mode 100644 plugins/geolocation/src/models.rs create mode 100644 plugins/geolocation/tsconfig.json create mode 100644 plugins/global-shortcut/CHANGELOG.md create mode 100644 plugins/global-shortcut/Cargo.toml create mode 100644 plugins/global-shortcut/LICENSE.spdx create mode 100644 plugins/global-shortcut/LICENSE_APACHE-2.0 create mode 100644 plugins/global-shortcut/LICENSE_MIT create mode 100644 plugins/global-shortcut/README.md create mode 100644 plugins/global-shortcut/SECURITY.md create mode 100644 plugins/global-shortcut/api-iife.js create mode 100644 plugins/global-shortcut/banner.png create mode 100644 plugins/global-shortcut/build.rs create mode 100644 plugins/global-shortcut/guest-js/index.ts create mode 100644 plugins/global-shortcut/package.json create mode 100644 plugins/global-shortcut/permissions/autogenerated/commands/is_registered.toml create mode 100644 plugins/global-shortcut/permissions/autogenerated/commands/register.toml create mode 100644 plugins/global-shortcut/permissions/autogenerated/commands/register_all.toml create mode 100644 plugins/global-shortcut/permissions/autogenerated/commands/unregister.toml create mode 100644 plugins/global-shortcut/permissions/autogenerated/commands/unregister_all.toml create mode 100644 plugins/global-shortcut/permissions/autogenerated/reference.md create mode 100644 plugins/global-shortcut/permissions/default.toml create mode 100644 plugins/global-shortcut/permissions/schemas/schema.json create mode 100644 plugins/global-shortcut/rollup.config.js create mode 100644 plugins/global-shortcut/src/error.rs create mode 100644 plugins/global-shortcut/src/lib.rs create mode 100644 plugins/global-shortcut/tsconfig.json create mode 100644 plugins/haptics/CHANGELOG.md create mode 100644 plugins/haptics/Cargo.toml create mode 100644 plugins/haptics/LICENSE.spdx create mode 100644 plugins/haptics/LICENSE_APACHE-2.0 create mode 100644 plugins/haptics/LICENSE_MIT create mode 100644 plugins/haptics/README.md create mode 100644 plugins/haptics/SECURITY.md create mode 100644 plugins/haptics/android/.gitignore create mode 100644 plugins/haptics/android/build.gradle.kts create mode 100644 plugins/haptics/android/consumer-rules.pro create mode 100644 plugins/haptics/android/settings.gradle create mode 100644 plugins/haptics/android/src/androidTest/java/ExampleInstrumentedTest.kt create mode 100644 plugins/haptics/android/src/main/AndroidManifest.xml create mode 100644 plugins/haptics/android/src/main/java/HapticsPlugin.kt create mode 100644 plugins/haptics/android/src/main/java/patterns/Impact.kt create mode 100644 plugins/haptics/android/src/main/java/patterns/Notification.kt create mode 100644 plugins/haptics/android/src/main/java/patterns/Pattern.kt create mode 100644 plugins/haptics/android/src/main/java/patterns/Selection.kt create mode 100644 plugins/haptics/android/src/test/java/ExampleUnitTest.kt create mode 100644 plugins/haptics/api-iife.js create mode 100644 plugins/haptics/build.rs create mode 100644 plugins/haptics/contributors/crabnebula.svg create mode 100644 plugins/haptics/contributors/rescue.png create mode 100644 plugins/haptics/guest-js/bindings.ts create mode 100644 plugins/haptics/guest-js/index.ts create mode 100644 plugins/haptics/ios/.gitignore create mode 100644 plugins/haptics/ios/Package.swift create mode 100644 plugins/haptics/ios/README.md create mode 100644 plugins/haptics/ios/Sources/HapticsPlugin.swift create mode 100644 plugins/haptics/ios/Tests/PluginTests/PluginTests.swift create mode 100644 plugins/haptics/package.json create mode 100644 plugins/haptics/permissions/autogenerated/commands/impact_feedback.toml create mode 100644 plugins/haptics/permissions/autogenerated/commands/notification_feedback.toml create mode 100644 plugins/haptics/permissions/autogenerated/commands/selection_feedback.toml create mode 100644 plugins/haptics/permissions/autogenerated/commands/vibrate.toml create mode 100644 plugins/haptics/permissions/autogenerated/reference.md create mode 100644 plugins/haptics/permissions/schemas/schema.json create mode 100644 plugins/haptics/rollup.config.js create mode 100644 plugins/haptics/src/commands.rs create mode 100644 plugins/haptics/src/desktop.rs create mode 100644 plugins/haptics/src/error.rs create mode 100644 plugins/haptics/src/lib.rs create mode 100644 plugins/haptics/src/mobile.rs create mode 100644 plugins/haptics/src/models.rs create mode 100644 plugins/haptics/tsconfig.json create mode 100644 plugins/http/CHANGELOG.md create mode 100644 plugins/http/Cargo.toml create mode 100644 plugins/http/LICENSE.spdx create mode 100644 plugins/http/LICENSE_APACHE-2.0 create mode 100644 plugins/http/LICENSE_MIT create mode 100644 plugins/http/README.md create mode 100644 plugins/http/SECURITY.md create mode 100644 plugins/http/api-iife.js create mode 100644 plugins/http/banner.png create mode 100644 plugins/http/build.rs create mode 100644 plugins/http/guest-js/index.ts create mode 100644 plugins/http/package.json create mode 100644 plugins/http/permissions/autogenerated/commands/fetch.toml create mode 100644 plugins/http/permissions/autogenerated/commands/fetch_cancel.toml create mode 100644 plugins/http/permissions/autogenerated/commands/fetch_cancel_body.toml create mode 100644 plugins/http/permissions/autogenerated/commands/fetch_read_body.toml create mode 100644 plugins/http/permissions/autogenerated/commands/fetch_send.toml create mode 100644 plugins/http/permissions/autogenerated/reference.md create mode 100644 plugins/http/permissions/default.toml create mode 100644 plugins/http/permissions/schemas/schema.json create mode 100644 plugins/http/rollup.config.js create mode 100644 plugins/http/src/commands.rs create mode 100644 plugins/http/src/config.rs create mode 100644 plugins/http/src/error.rs create mode 100644 plugins/http/src/lib.rs create mode 100644 plugins/http/src/reqwest_cookie_store.rs create mode 100644 plugins/http/src/scope.rs create mode 100644 plugins/http/tsconfig.json create mode 100644 plugins/localhost/CHANGELOG.md create mode 100644 plugins/localhost/Cargo.toml create mode 100644 plugins/localhost/LICENSE.spdx create mode 100644 plugins/localhost/LICENSE_APACHE-2.0 create mode 100644 plugins/localhost/LICENSE_MIT create mode 100644 plugins/localhost/README.md create mode 100644 plugins/localhost/SECURITY.md create mode 100644 plugins/localhost/banner.png create mode 100644 plugins/localhost/src/lib.rs create mode 100644 plugins/log/CHANGELOG.md create mode 100644 plugins/log/Cargo.toml create mode 100644 plugins/log/LICENSE.spdx create mode 100644 plugins/log/LICENSE_APACHE-2.0 create mode 100644 plugins/log/LICENSE_MIT create mode 100644 plugins/log/README.md create mode 100644 plugins/log/SECURITY.md create mode 100644 plugins/log/api-iife.js create mode 100644 plugins/log/banner.png create mode 100644 plugins/log/build.rs create mode 100644 plugins/log/guest-js/index.ts create mode 100644 plugins/log/ios/.gitignore create mode 100644 plugins/log/ios/Package.swift create mode 100644 plugins/log/ios/README.md create mode 100644 plugins/log/ios/Sources/LogPlugin.swift create mode 100644 plugins/log/package.json create mode 100644 plugins/log/permissions/autogenerated/commands/log.toml create mode 100644 plugins/log/permissions/autogenerated/reference.md create mode 100644 plugins/log/permissions/default.toml create mode 100644 plugins/log/permissions/schemas/schema.json create mode 100644 plugins/log/rollup.config.js create mode 100644 plugins/log/src/commands.rs create mode 100644 plugins/log/src/lib.rs create mode 100644 plugins/log/tsconfig.json create mode 100644 plugins/mirrors.txt create mode 100644 plugins/nfc/CHANGELOG.md create mode 100644 plugins/nfc/Cargo.toml create mode 100644 plugins/nfc/LICENSE.spdx create mode 100644 plugins/nfc/LICENSE_APACHE-2.0 create mode 100644 plugins/nfc/LICENSE_MIT create mode 100644 plugins/nfc/README.md create mode 100644 plugins/nfc/SECURITY.md create mode 100644 plugins/nfc/android/.gitignore create mode 100644 plugins/nfc/android/build.gradle.kts create mode 100644 plugins/nfc/android/consumer-rules.pro create mode 100644 plugins/nfc/android/settings.gradle create mode 100644 plugins/nfc/android/src/androidTest/java/ExampleInstrumentedTest.kt create mode 100644 plugins/nfc/android/src/main/AndroidManifest.xml create mode 100644 plugins/nfc/android/src/main/java/NfcPlugin.kt create mode 100644 plugins/nfc/android/src/main/res/xml/nfc_tech_filter.xml create mode 100644 plugins/nfc/android/src/test/java/ExampleUnitTest.kt create mode 100644 plugins/nfc/api-iife.js create mode 100644 plugins/nfc/build.rs create mode 100644 plugins/nfc/contributors/crabnebula.svg create mode 100644 plugins/nfc/contributors/impierce.svg create mode 100644 plugins/nfc/guest-js/index.ts create mode 100644 plugins/nfc/ios/.gitignore create mode 100644 plugins/nfc/ios/Package.swift create mode 100644 plugins/nfc/ios/README.md create mode 100644 plugins/nfc/ios/Sources/NfcPlugin.swift create mode 100644 plugins/nfc/ios/Tests/PluginTests/PluginTests.swift create mode 100644 plugins/nfc/package.json create mode 100644 plugins/nfc/permissions/autogenerated/commands/is_available.toml create mode 100644 plugins/nfc/permissions/autogenerated/commands/scan.toml create mode 100644 plugins/nfc/permissions/autogenerated/commands/write.toml create mode 100644 plugins/nfc/permissions/autogenerated/reference.md create mode 100644 plugins/nfc/permissions/default.toml create mode 100644 plugins/nfc/permissions/schemas/schema.json create mode 100644 plugins/nfc/rollup.config.js create mode 100644 plugins/nfc/src/error.rs create mode 100644 plugins/nfc/src/lib.rs create mode 100644 plugins/nfc/src/mobile.rs create mode 100644 plugins/nfc/src/models.rs create mode 100644 plugins/nfc/tsconfig.json create mode 100644 plugins/notification/CHANGELOG.md create mode 100644 plugins/notification/Cargo.toml create mode 100644 plugins/notification/LICENSE.spdx create mode 100644 plugins/notification/LICENSE_APACHE-2.0 create mode 100644 plugins/notification/LICENSE_MIT create mode 100644 plugins/notification/README.md create mode 100644 plugins/notification/SECURITY.md create mode 100644 plugins/notification/android/.gitignore create mode 100644 plugins/notification/android/build.gradle.kts create mode 100644 plugins/notification/android/consumer-rules.pro create mode 100644 plugins/notification/android/settings.gradle create mode 100644 plugins/notification/android/src/androidTest/java/ExampleInstrumentedTest.kt create mode 100644 plugins/notification/android/src/main/AndroidManifest.xml create mode 100644 plugins/notification/android/src/main/java/AssetUtils.kt create mode 100644 plugins/notification/android/src/main/java/ChannelManager.kt create mode 100644 plugins/notification/android/src/main/java/Notification.kt create mode 100644 plugins/notification/android/src/main/java/NotificationAttachment.kt create mode 100644 plugins/notification/android/src/main/java/NotificationPlugin.kt create mode 100644 plugins/notification/android/src/main/java/NotificationSchedule.kt create mode 100644 plugins/notification/android/src/main/java/NotificationStorage.kt create mode 100644 plugins/notification/android/src/main/java/TauriNotificationManager.kt create mode 100644 plugins/notification/android/src/main/res/drawable/ic_transparent.xml create mode 100644 plugins/notification/android/src/test/java/ExampleUnitTest.kt create mode 100644 plugins/notification/api-iife.js create mode 100644 plugins/notification/banner.png create mode 100644 plugins/notification/build.rs create mode 100644 plugins/notification/guest-js/index.ts create mode 100644 plugins/notification/guest-js/init.ts create mode 100644 plugins/notification/ios/.gitignore create mode 100644 plugins/notification/ios/Package.swift create mode 100644 plugins/notification/ios/README.md create mode 100644 plugins/notification/ios/Sources/Notification.swift create mode 100644 plugins/notification/ios/Sources/NotificationCategory.swift create mode 100644 plugins/notification/ios/Sources/NotificationHandler.swift create mode 100644 plugins/notification/ios/Sources/NotificationManager.swift create mode 100644 plugins/notification/ios/Sources/NotificationPlugin.swift create mode 100644 plugins/notification/ios/Tests/PluginTests/PluginTests.swift create mode 100644 plugins/notification/package.json create mode 100644 plugins/notification/permissions/autogenerated/commands/batch.toml create mode 100644 plugins/notification/permissions/autogenerated/commands/cancel.toml create mode 100644 plugins/notification/permissions/autogenerated/commands/check_permissions.toml create mode 100644 plugins/notification/permissions/autogenerated/commands/create_channel.toml create mode 100644 plugins/notification/permissions/autogenerated/commands/delete_channel.toml create mode 100644 plugins/notification/permissions/autogenerated/commands/get_active.toml create mode 100644 plugins/notification/permissions/autogenerated/commands/get_pending.toml create mode 100644 plugins/notification/permissions/autogenerated/commands/is_permission_granted.toml create mode 100644 plugins/notification/permissions/autogenerated/commands/list_channels.toml create mode 100644 plugins/notification/permissions/autogenerated/commands/notify.toml create mode 100644 plugins/notification/permissions/autogenerated/commands/permission_state.toml create mode 100644 plugins/notification/permissions/autogenerated/commands/register_action_types.toml create mode 100644 plugins/notification/permissions/autogenerated/commands/register_listener.toml create mode 100644 plugins/notification/permissions/autogenerated/commands/remove_active.toml create mode 100644 plugins/notification/permissions/autogenerated/commands/remove_listener.toml create mode 100644 plugins/notification/permissions/autogenerated/commands/request_permission.toml create mode 100644 plugins/notification/permissions/autogenerated/commands/show.toml create mode 100644 plugins/notification/permissions/autogenerated/reference.md create mode 100644 plugins/notification/permissions/default.toml create mode 100644 plugins/notification/permissions/schemas/schema.json create mode 100644 plugins/notification/rollup.config.js create mode 100644 plugins/notification/src/commands.rs create mode 100644 plugins/notification/src/desktop.rs create mode 100644 plugins/notification/src/error.rs create mode 100644 plugins/notification/src/init-iife.js create mode 100644 plugins/notification/src/lib.rs create mode 100644 plugins/notification/src/mobile.rs create mode 100644 plugins/notification/src/models.rs create mode 100644 plugins/notification/test/tauri.conf.json create mode 100644 plugins/notification/tsconfig.json create mode 100644 plugins/opener/CHANGELOG.md create mode 100644 plugins/opener/Cargo.toml create mode 100644 plugins/opener/LICENSE.spdx create mode 100644 plugins/opener/LICENSE_APACHE-2.0 create mode 100644 plugins/opener/LICENSE_MIT create mode 100644 plugins/opener/README.md create mode 100644 plugins/opener/SECURITY.md create mode 100644 plugins/opener/android/.gitignore create mode 100644 plugins/opener/android/build.gradle.kts create mode 100644 plugins/opener/android/consumer-rules.pro create mode 100644 plugins/opener/android/settings.gradle create mode 100644 plugins/opener/android/src/main/AndroidManifest.xml create mode 100644 plugins/opener/android/src/main/java/OpenerPlugin.kt create mode 100644 plugins/opener/api-iife.js create mode 100644 plugins/opener/build.rs create mode 100644 plugins/opener/guest-js/index.ts create mode 100644 plugins/opener/guest-js/init.ts create mode 100644 plugins/opener/ios/Package.resolved create mode 100644 plugins/opener/ios/Package.swift create mode 100644 plugins/opener/ios/Sources/OpenerPlugin.swift create mode 100644 plugins/opener/package.json create mode 100644 plugins/opener/permissions/allow-default-urls.toml create mode 100644 plugins/opener/permissions/autogenerated/commands/open_path.toml create mode 100644 plugins/opener/permissions/autogenerated/commands/open_url.toml create mode 100644 plugins/opener/permissions/autogenerated/commands/reveal_item_in_dir.toml create mode 100644 plugins/opener/permissions/autogenerated/reference.md create mode 100644 plugins/opener/permissions/default.toml create mode 100644 plugins/opener/permissions/schemas/schema.json create mode 100644 plugins/opener/rollup.config.js create mode 100644 plugins/opener/src/commands.rs create mode 100644 plugins/opener/src/config.rs create mode 100644 plugins/opener/src/error.rs create mode 100644 plugins/opener/src/init-iife.js create mode 100644 plugins/opener/src/lib.rs create mode 100644 plugins/opener/src/open.rs create mode 100644 plugins/opener/src/reveal_item_in_dir.rs create mode 100644 plugins/opener/src/scope.rs create mode 100644 plugins/opener/src/scope_entry.rs create mode 100644 plugins/opener/src/windows_shell_path.rs create mode 100644 plugins/opener/tsconfig.json create mode 100644 plugins/os/CHANGELOG.md create mode 100644 plugins/os/Cargo.toml create mode 100644 plugins/os/LICENSE.spdx create mode 100644 plugins/os/LICENSE_APACHE-2.0 create mode 100644 plugins/os/LICENSE_MIT create mode 100644 plugins/os/README.md create mode 100644 plugins/os/SECURITY.md create mode 100644 plugins/os/api-iife.js create mode 100644 plugins/os/banner.png create mode 100644 plugins/os/build.rs create mode 100644 plugins/os/guest-js/index.ts create mode 100644 plugins/os/package.json create mode 100644 plugins/os/permissions/autogenerated/commands/arch.toml create mode 100644 plugins/os/permissions/autogenerated/commands/exe_extension.toml create mode 100644 plugins/os/permissions/autogenerated/commands/family.toml create mode 100644 plugins/os/permissions/autogenerated/commands/hostname.toml create mode 100644 plugins/os/permissions/autogenerated/commands/locale.toml create mode 100644 plugins/os/permissions/autogenerated/commands/os_type.toml create mode 100644 plugins/os/permissions/autogenerated/commands/platform.toml create mode 100644 plugins/os/permissions/autogenerated/commands/version.toml create mode 100644 plugins/os/permissions/autogenerated/reference.md create mode 100644 plugins/os/permissions/default.toml create mode 100644 plugins/os/permissions/schemas/schema.json create mode 100644 plugins/os/rollup.config.js create mode 100644 plugins/os/src/commands.rs create mode 100644 plugins/os/src/error.rs create mode 100644 plugins/os/src/init.js create mode 100644 plugins/os/src/lib.rs create mode 100644 plugins/os/tsconfig.json create mode 100644 plugins/persisted-scope/CHANGELOG.md create mode 100644 plugins/persisted-scope/Cargo.toml create mode 100644 plugins/persisted-scope/LICENSE.spdx create mode 100644 plugins/persisted-scope/LICENSE_APACHE-2.0 create mode 100644 plugins/persisted-scope/LICENSE_MIT create mode 100644 plugins/persisted-scope/README.md create mode 100644 plugins/persisted-scope/SECURITY.md create mode 100644 plugins/persisted-scope/banner.png create mode 100644 plugins/persisted-scope/src/lib.rs create mode 100644 plugins/positioner/CHANGELOG.md create mode 100644 plugins/positioner/Cargo.toml create mode 100644 plugins/positioner/LICENSE.spdx create mode 100644 plugins/positioner/LICENSE_APACHE-2.0 create mode 100644 plugins/positioner/LICENSE_MIT create mode 100644 plugins/positioner/README.md create mode 100644 plugins/positioner/SECURITY.md create mode 100644 plugins/positioner/api-iife.js create mode 100644 plugins/positioner/banner.png create mode 100644 plugins/positioner/build.rs create mode 100644 plugins/positioner/guest-js/index.ts create mode 100644 plugins/positioner/package.json create mode 100644 plugins/positioner/permissions/autogenerated/commands/move_window.toml create mode 100644 plugins/positioner/permissions/autogenerated/commands/move_window_constrained.toml create mode 100644 plugins/positioner/permissions/autogenerated/commands/set_tray_icon_state.toml create mode 100644 plugins/positioner/permissions/autogenerated/reference.md create mode 100644 plugins/positioner/permissions/default.toml create mode 100644 plugins/positioner/permissions/schemas/schema.json create mode 100644 plugins/positioner/rollup.config.js create mode 100644 plugins/positioner/src/ext.rs create mode 100644 plugins/positioner/src/lib.rs create mode 100644 plugins/positioner/tsconfig.json create mode 100644 plugins/process/CHANGELOG.md create mode 100644 plugins/process/Cargo.toml create mode 100644 plugins/process/LICENSE.spdx create mode 100644 plugins/process/LICENSE_APACHE-2.0 create mode 100644 plugins/process/LICENSE_MIT create mode 100644 plugins/process/README.md create mode 100644 plugins/process/SECURITY.md create mode 100644 plugins/process/api-iife.js create mode 100644 plugins/process/banner.png create mode 100644 plugins/process/build.rs create mode 100644 plugins/process/guest-js/index.ts create mode 100644 plugins/process/package.json create mode 100644 plugins/process/permissions/autogenerated/commands/exit.toml create mode 100644 plugins/process/permissions/autogenerated/commands/restart.toml create mode 100644 plugins/process/permissions/autogenerated/reference.md create mode 100644 plugins/process/permissions/default.toml create mode 100644 plugins/process/permissions/schemas/schema.json create mode 100644 plugins/process/rollup.config.js create mode 100644 plugins/process/src/commands.rs create mode 100644 plugins/process/src/lib.rs create mode 100644 plugins/process/tsconfig.json create mode 100644 plugins/shell/CHANGELOG.md create mode 100644 plugins/shell/Cargo.toml create mode 100644 plugins/shell/LICENSE.spdx create mode 100644 plugins/shell/LICENSE_APACHE-2.0 create mode 100644 plugins/shell/LICENSE_MIT create mode 100644 plugins/shell/README.md create mode 100644 plugins/shell/SECURITY.md create mode 100644 plugins/shell/android/.gitignore create mode 100644 plugins/shell/android/build.gradle.kts create mode 100644 plugins/shell/android/consumer-rules.pro create mode 100644 plugins/shell/android/settings.gradle create mode 100644 plugins/shell/android/src/main/AndroidManifest.xml create mode 100644 plugins/shell/android/src/main/java/ShellPlugin.kt create mode 100644 plugins/shell/api-iife.js create mode 100644 plugins/shell/banner.png create mode 100644 plugins/shell/build.rs create mode 100644 plugins/shell/guest-js/index.ts create mode 100644 plugins/shell/guest-js/init.ts create mode 100644 plugins/shell/ios/Package.resolved create mode 100644 plugins/shell/ios/Package.swift create mode 100644 plugins/shell/ios/Sources/ShellPlugin.swift create mode 100644 plugins/shell/package.json create mode 100644 plugins/shell/permissions/autogenerated/commands/execute.toml create mode 100644 plugins/shell/permissions/autogenerated/commands/kill.toml create mode 100644 plugins/shell/permissions/autogenerated/commands/open.toml create mode 100644 plugins/shell/permissions/autogenerated/commands/spawn.toml create mode 100644 plugins/shell/permissions/autogenerated/commands/stdin_write.toml create mode 100644 plugins/shell/permissions/autogenerated/reference.md create mode 100644 plugins/shell/permissions/default.toml create mode 100644 plugins/shell/permissions/schemas/schema.json create mode 100644 plugins/shell/rollup.config.js create mode 100644 plugins/shell/src/commands.rs create mode 100644 plugins/shell/src/config.rs create mode 100644 plugins/shell/src/error.rs create mode 100644 plugins/shell/src/init-iife.js create mode 100644 plugins/shell/src/lib.rs create mode 100644 plugins/shell/src/open.rs create mode 100644 plugins/shell/src/process/mod.rs create mode 100644 plugins/shell/src/scope.rs create mode 100644 plugins/shell/src/scope_entry.rs create mode 100644 plugins/shell/test/test.txt create mode 100644 plugins/shell/tsconfig.json create mode 100644 plugins/single-instance/CHANGELOG.md create mode 100644 plugins/single-instance/Cargo.toml create mode 100644 plugins/single-instance/LICENSE.spdx create mode 100644 plugins/single-instance/LICENSE_APACHE-2.0 create mode 100644 plugins/single-instance/LICENSE_MIT create mode 100644 plugins/single-instance/README.md create mode 100644 plugins/single-instance/SECURITY.md create mode 100644 plugins/single-instance/banner.png create mode 100644 plugins/single-instance/examples/vanilla/.gitignore create mode 100644 plugins/single-instance/examples/vanilla/package.json create mode 100644 plugins/single-instance/examples/vanilla/public/index.html create mode 100644 plugins/single-instance/examples/vanilla/src-tauri/.gitignore create mode 100644 plugins/single-instance/examples/vanilla/src-tauri/Cargo.toml create mode 100644 plugins/single-instance/examples/vanilla/src-tauri/build.rs create mode 100644 plugins/single-instance/examples/vanilla/src-tauri/icons/128x128.png create mode 100644 plugins/single-instance/examples/vanilla/src-tauri/icons/128x128@2x.png create mode 100644 plugins/single-instance/examples/vanilla/src-tauri/icons/32x32.png create mode 100644 plugins/single-instance/examples/vanilla/src-tauri/icons/icon.ico create mode 100644 plugins/single-instance/examples/vanilla/src-tauri/icons/icon.png create mode 100644 plugins/single-instance/examples/vanilla/src-tauri/src/main.rs create mode 100644 plugins/single-instance/examples/vanilla/src-tauri/tauri.conf.json create mode 100644 plugins/single-instance/src/lib.rs create mode 100644 plugins/single-instance/src/platform_impl/linux.rs create mode 100644 plugins/single-instance/src/platform_impl/macos.rs create mode 100644 plugins/single-instance/src/platform_impl/windows.rs create mode 100644 plugins/single-instance/src/semver_compat.rs create mode 100644 plugins/sql/CHANGELOG.md create mode 100644 plugins/sql/Cargo.toml create mode 100644 plugins/sql/LICENSE.spdx create mode 100644 plugins/sql/LICENSE_APACHE-2.0 create mode 100644 plugins/sql/LICENSE_MIT create mode 100644 plugins/sql/README.md create mode 100644 plugins/sql/SECURITY.md create mode 100644 plugins/sql/api-iife.js create mode 100644 plugins/sql/banner.png create mode 100644 plugins/sql/build.rs create mode 100644 plugins/sql/guest-js/index.ts create mode 100644 plugins/sql/package.json create mode 100644 plugins/sql/permissions/autogenerated/commands/close.toml create mode 100644 plugins/sql/permissions/autogenerated/commands/execute.toml create mode 100644 plugins/sql/permissions/autogenerated/commands/load.toml create mode 100644 plugins/sql/permissions/autogenerated/commands/select.toml create mode 100644 plugins/sql/permissions/autogenerated/reference.md create mode 100644 plugins/sql/permissions/default.toml create mode 100644 plugins/sql/permissions/schemas/schema.json create mode 100644 plugins/sql/rollup.config.js create mode 100644 plugins/sql/src/commands.rs create mode 100644 plugins/sql/src/decode/mod.rs create mode 100644 plugins/sql/src/decode/mysql.rs create mode 100644 plugins/sql/src/decode/postgres.rs create mode 100644 plugins/sql/src/decode/sqlite.rs create mode 100644 plugins/sql/src/error.rs create mode 100644 plugins/sql/src/lib.rs create mode 100644 plugins/sql/src/wrapper.rs create mode 100644 plugins/sql/tsconfig.json create mode 100644 plugins/store/CHANGELOG.md create mode 100644 plugins/store/Cargo.toml create mode 100644 plugins/store/LICENSE.spdx create mode 100644 plugins/store/LICENSE_APACHE-2.0 create mode 100644 plugins/store/LICENSE_MIT create mode 100644 plugins/store/README.md create mode 100644 plugins/store/SECURITY.md create mode 100644 plugins/store/api-iife.js create mode 100644 plugins/store/banner.png create mode 100644 plugins/store/build.rs create mode 100644 plugins/store/examples/AppSettingsManager/.gitignore create mode 100644 plugins/store/examples/AppSettingsManager/.vscode/extensions.json create mode 100644 plugins/store/examples/AppSettingsManager/README.md create mode 100644 plugins/store/examples/AppSettingsManager/index.html create mode 100644 plugins/store/examples/AppSettingsManager/package.json create mode 100644 plugins/store/examples/AppSettingsManager/src-tauri/.gitignore create mode 100644 plugins/store/examples/AppSettingsManager/src-tauri/Cargo.toml create mode 100644 plugins/store/examples/AppSettingsManager/src-tauri/build.rs create mode 100644 plugins/store/examples/AppSettingsManager/src-tauri/icons/128x128.png create mode 100644 plugins/store/examples/AppSettingsManager/src-tauri/icons/128x128@2x.png create mode 100644 plugins/store/examples/AppSettingsManager/src-tauri/icons/32x32.png create mode 100644 plugins/store/examples/AppSettingsManager/src-tauri/icons/Square107x107Logo.png create mode 100644 plugins/store/examples/AppSettingsManager/src-tauri/icons/Square142x142Logo.png create mode 100644 plugins/store/examples/AppSettingsManager/src-tauri/icons/Square150x150Logo.png create mode 100644 plugins/store/examples/AppSettingsManager/src-tauri/icons/Square284x284Logo.png create mode 100644 plugins/store/examples/AppSettingsManager/src-tauri/icons/Square30x30Logo.png create mode 100644 plugins/store/examples/AppSettingsManager/src-tauri/icons/Square310x310Logo.png create mode 100644 plugins/store/examples/AppSettingsManager/src-tauri/icons/Square44x44Logo.png create mode 100644 plugins/store/examples/AppSettingsManager/src-tauri/icons/Square71x71Logo.png create mode 100644 plugins/store/examples/AppSettingsManager/src-tauri/icons/Square89x89Logo.png create mode 100644 plugins/store/examples/AppSettingsManager/src-tauri/icons/StoreLogo.png create mode 100644 plugins/store/examples/AppSettingsManager/src-tauri/icons/icon.icns create mode 100644 plugins/store/examples/AppSettingsManager/src-tauri/icons/icon.ico create mode 100644 plugins/store/examples/AppSettingsManager/src-tauri/icons/icon.png create mode 100644 plugins/store/examples/AppSettingsManager/src-tauri/src/app/mod.rs create mode 100644 plugins/store/examples/AppSettingsManager/src-tauri/src/app/settings.rs create mode 100644 plugins/store/examples/AppSettingsManager/src-tauri/src/main.rs create mode 100644 plugins/store/examples/AppSettingsManager/src-tauri/tauri.conf.json create mode 100644 plugins/store/examples/AppSettingsManager/src/assets/tauri.svg create mode 100644 plugins/store/examples/AppSettingsManager/src/assets/typescript.svg create mode 100644 plugins/store/examples/AppSettingsManager/src/assets/vite.svg create mode 100644 plugins/store/examples/AppSettingsManager/src/main.ts create mode 100644 plugins/store/examples/AppSettingsManager/src/styles.css create mode 100644 plugins/store/examples/AppSettingsManager/tsconfig.json create mode 100644 plugins/store/examples/AppSettingsManager/vite.config.ts create mode 100644 plugins/store/guest-js/index.ts create mode 100644 plugins/store/package.json create mode 100644 plugins/store/permissions/autogenerated/commands/clear.toml create mode 100644 plugins/store/permissions/autogenerated/commands/delete.toml create mode 100644 plugins/store/permissions/autogenerated/commands/entries.toml create mode 100644 plugins/store/permissions/autogenerated/commands/get.toml create mode 100644 plugins/store/permissions/autogenerated/commands/get_store.toml create mode 100644 plugins/store/permissions/autogenerated/commands/has.toml create mode 100644 plugins/store/permissions/autogenerated/commands/keys.toml create mode 100644 plugins/store/permissions/autogenerated/commands/length.toml create mode 100644 plugins/store/permissions/autogenerated/commands/load.toml create mode 100644 plugins/store/permissions/autogenerated/commands/reload.toml create mode 100644 plugins/store/permissions/autogenerated/commands/reset.toml create mode 100644 plugins/store/permissions/autogenerated/commands/save.toml create mode 100644 plugins/store/permissions/autogenerated/commands/set.toml create mode 100644 plugins/store/permissions/autogenerated/commands/values.toml create mode 100644 plugins/store/permissions/autogenerated/reference.md create mode 100644 plugins/store/permissions/default.toml create mode 100644 plugins/store/permissions/schemas/schema.json create mode 100644 plugins/store/rollup.config.js create mode 100644 plugins/store/src/error.rs create mode 100644 plugins/store/src/lib.rs create mode 100644 plugins/store/src/store.rs create mode 100644 plugins/store/tsconfig.json create mode 100644 plugins/stronghold/CHANGELOG.md create mode 100644 plugins/stronghold/Cargo.toml create mode 100644 plugins/stronghold/LICENSE.spdx create mode 100644 plugins/stronghold/LICENSE_APACHE-2.0 create mode 100644 plugins/stronghold/LICENSE_MIT create mode 100644 plugins/stronghold/README.md create mode 100644 plugins/stronghold/SECURITY.md create mode 100644 plugins/stronghold/api-iife.js create mode 100644 plugins/stronghold/banner.png create mode 100644 plugins/stronghold/build.rs create mode 100644 plugins/stronghold/guest-js/index.ts create mode 100644 plugins/stronghold/package.json create mode 100644 plugins/stronghold/permissions/autogenerated/commands/create_client.toml create mode 100644 plugins/stronghold/permissions/autogenerated/commands/destroy.toml create mode 100644 plugins/stronghold/permissions/autogenerated/commands/execute_procedure.toml create mode 100644 plugins/stronghold/permissions/autogenerated/commands/get_store_record.toml create mode 100644 plugins/stronghold/permissions/autogenerated/commands/initialize.toml create mode 100644 plugins/stronghold/permissions/autogenerated/commands/load_client.toml create mode 100644 plugins/stronghold/permissions/autogenerated/commands/remove_secret.toml create mode 100644 plugins/stronghold/permissions/autogenerated/commands/remove_store_record.toml create mode 100644 plugins/stronghold/permissions/autogenerated/commands/save.toml create mode 100644 plugins/stronghold/permissions/autogenerated/commands/save_secret.toml create mode 100644 plugins/stronghold/permissions/autogenerated/commands/save_store_record.toml create mode 100644 plugins/stronghold/permissions/autogenerated/reference.md create mode 100644 plugins/stronghold/permissions/default.toml create mode 100644 plugins/stronghold/permissions/schemas/schema.json create mode 100644 plugins/stronghold/rollup.config.js create mode 100644 plugins/stronghold/src/kdf.rs create mode 100644 plugins/stronghold/src/lib.rs create mode 100644 plugins/stronghold/src/stronghold.rs create mode 100644 plugins/stronghold/tsconfig.json create mode 100644 plugins/updater/CHANGELOG.md create mode 100644 plugins/updater/Cargo.toml create mode 100644 plugins/updater/LICENSE.spdx create mode 100644 plugins/updater/LICENSE_APACHE-2.0 create mode 100644 plugins/updater/LICENSE_MIT create mode 100644 plugins/updater/README.md create mode 100644 plugins/updater/SECURITY.md create mode 100644 plugins/updater/api-iife.js create mode 100644 plugins/updater/banner.png create mode 100644 plugins/updater/build.rs create mode 100644 plugins/updater/guest-js/index.ts create mode 100644 plugins/updater/package.json create mode 100644 plugins/updater/permissions/autogenerated/commands/check.toml create mode 100644 plugins/updater/permissions/autogenerated/commands/download.toml create mode 100644 plugins/updater/permissions/autogenerated/commands/download_and_install.toml create mode 100644 plugins/updater/permissions/autogenerated/commands/install.toml create mode 100644 plugins/updater/permissions/autogenerated/reference.md create mode 100644 plugins/updater/permissions/default.toml create mode 100644 plugins/updater/permissions/schemas/schema.json create mode 100644 plugins/updater/rollup.config.js create mode 100644 plugins/updater/src/commands.rs create mode 100644 plugins/updater/src/config.rs create mode 100644 plugins/updater/src/error.rs create mode 100644 plugins/updater/src/lib.rs create mode 100644 plugins/updater/src/updater.rs create mode 100644 plugins/updater/tests/app-updater/.gitignore create mode 100644 plugins/updater/tests/app-updater/Cargo.toml create mode 100644 plugins/updater/tests/app-updater/build.rs create mode 100644 plugins/updater/tests/app-updater/icons/128x128.png create mode 100644 plugins/updater/tests/app-updater/icons/128x128@2x.png create mode 100644 plugins/updater/tests/app-updater/icons/32x32.png create mode 100644 plugins/updater/tests/app-updater/icons/icon.ico create mode 100644 plugins/updater/tests/app-updater/icons/icon.png create mode 100644 plugins/updater/tests/app-updater/src/main.rs create mode 100644 plugins/updater/tests/app-updater/tauri.conf.json create mode 100644 plugins/updater/tests/app-updater/tests/update.rs create mode 100644 plugins/updater/tests/updater-migration/Cargo.toml create mode 100644 plugins/updater/tests/updater-migration/icons/128x128.png create mode 100644 plugins/updater/tests/updater-migration/icons/128x128@2x.png create mode 100644 plugins/updater/tests/updater-migration/icons/32x32.png create mode 100644 plugins/updater/tests/updater-migration/icons/icon.ico create mode 100644 plugins/updater/tests/updater-migration/icons/icon.png create mode 100644 plugins/updater/tests/updater-migration/tests/update.rs create mode 100644 plugins/updater/tests/updater-migration/v1-app/.gitignore create mode 100644 plugins/updater/tests/updater-migration/v1-app/Cargo.lock create mode 100644 plugins/updater/tests/updater-migration/v1-app/Cargo.toml create mode 100644 plugins/updater/tests/updater-migration/v1-app/build.rs create mode 100644 plugins/updater/tests/updater-migration/v1-app/package.json create mode 100644 plugins/updater/tests/updater-migration/v1-app/src/main.rs create mode 100644 plugins/updater/tests/updater-migration/v1-app/tauri.conf.json create mode 100644 plugins/updater/tests/updater-migration/v2-app/.gitignore create mode 100644 plugins/updater/tests/updater-migration/v2-app/Cargo.toml create mode 100644 plugins/updater/tests/updater-migration/v2-app/build.rs create mode 100644 plugins/updater/tests/updater-migration/v2-app/src/main.rs create mode 100644 plugins/updater/tests/updater-migration/v2-app/tauri.conf.json create mode 100644 plugins/updater/tsconfig.json create mode 100644 plugins/upload/CHANGELOG.md create mode 100644 plugins/upload/Cargo.toml create mode 100644 plugins/upload/LICENSE.spdx create mode 100644 plugins/upload/LICENSE_APACHE-2.0 create mode 100644 plugins/upload/LICENSE_MIT create mode 100644 plugins/upload/README.md create mode 100644 plugins/upload/SECURITY.md create mode 100644 plugins/upload/api-iife.js create mode 100644 plugins/upload/banner.png create mode 100644 plugins/upload/build.rs create mode 100644 plugins/upload/guest-js/index.ts create mode 100644 plugins/upload/package.json create mode 100644 plugins/upload/permissions/autogenerated/commands/download.toml create mode 100644 plugins/upload/permissions/autogenerated/commands/upload.toml create mode 100644 plugins/upload/permissions/autogenerated/reference.md create mode 100644 plugins/upload/permissions/default.toml create mode 100644 plugins/upload/permissions/schemas/schema.json create mode 100644 plugins/upload/rollup.config.js create mode 100644 plugins/upload/src/lib.rs create mode 100644 plugins/upload/src/transfer_stats.rs create mode 100644 plugins/upload/test/download.txt create mode 100644 plugins/upload/test/upload.txt create mode 100644 plugins/upload/tsconfig.json create mode 100644 plugins/websocket/CHANGELOG.md create mode 100644 plugins/websocket/Cargo.toml create mode 100644 plugins/websocket/LICENSE.spdx create mode 100644 plugins/websocket/LICENSE_APACHE-2.0 create mode 100644 plugins/websocket/LICENSE_MIT create mode 100644 plugins/websocket/README.md create mode 100644 plugins/websocket/SECURITY.md create mode 100644 plugins/websocket/api-iife.js create mode 100644 plugins/websocket/banner.png create mode 100644 plugins/websocket/build.rs create mode 100644 plugins/websocket/examples/tauri-app/.gitignore create mode 100644 plugins/websocket/examples/tauri-app/index.html create mode 100644 plugins/websocket/examples/tauri-app/package.json create mode 100644 plugins/websocket/examples/tauri-app/public/vite.svg create mode 100644 plugins/websocket/examples/tauri-app/src-tauri/.gitignore create mode 100644 plugins/websocket/examples/tauri-app/src-tauri/Cargo.toml create mode 100644 plugins/websocket/examples/tauri-app/src-tauri/build.rs create mode 100644 plugins/websocket/examples/tauri-app/src-tauri/icons/128x128.png create mode 100644 plugins/websocket/examples/tauri-app/src-tauri/icons/128x128@2x.png create mode 100644 plugins/websocket/examples/tauri-app/src-tauri/icons/32x32.png create mode 100644 plugins/websocket/examples/tauri-app/src-tauri/icons/Square107x107Logo.png create mode 100644 plugins/websocket/examples/tauri-app/src-tauri/icons/Square142x142Logo.png create mode 100644 plugins/websocket/examples/tauri-app/src-tauri/icons/Square150x150Logo.png create mode 100644 plugins/websocket/examples/tauri-app/src-tauri/icons/Square284x284Logo.png create mode 100644 plugins/websocket/examples/tauri-app/src-tauri/icons/Square30x30Logo.png create mode 100644 plugins/websocket/examples/tauri-app/src-tauri/icons/Square310x310Logo.png create mode 100644 plugins/websocket/examples/tauri-app/src-tauri/icons/Square44x44Logo.png create mode 100644 plugins/websocket/examples/tauri-app/src-tauri/icons/Square71x71Logo.png create mode 100644 plugins/websocket/examples/tauri-app/src-tauri/icons/Square89x89Logo.png create mode 100644 plugins/websocket/examples/tauri-app/src-tauri/icons/StoreLogo.png create mode 100644 plugins/websocket/examples/tauri-app/src-tauri/icons/icon.ico create mode 100644 plugins/websocket/examples/tauri-app/src-tauri/icons/icon.png create mode 100644 plugins/websocket/examples/tauri-app/src-tauri/rustfmt.toml create mode 100644 plugins/websocket/examples/tauri-app/src-tauri/src/main.rs create mode 100644 plugins/websocket/examples/tauri-app/src-tauri/tauri.conf.json create mode 100644 plugins/websocket/examples/tauri-app/src/main.ts create mode 100644 plugins/websocket/examples/tauri-app/src/style.css create mode 100644 plugins/websocket/examples/tauri-app/src/typescript.svg create mode 100644 plugins/websocket/examples/tauri-app/src/vite-env.d.ts create mode 100644 plugins/websocket/examples/tauri-app/tsconfig.json create mode 100644 plugins/websocket/guest-js/index.ts create mode 100644 plugins/websocket/package.json create mode 100644 plugins/websocket/permissions/autogenerated/commands/connect.toml create mode 100644 plugins/websocket/permissions/autogenerated/commands/send.toml create mode 100644 plugins/websocket/permissions/autogenerated/reference.md create mode 100644 plugins/websocket/permissions/default.toml create mode 100644 plugins/websocket/permissions/schemas/schema.json create mode 100644 plugins/websocket/rollup.config.js create mode 100644 plugins/websocket/src/lib.rs create mode 100644 plugins/websocket/tsconfig.json create mode 100644 plugins/window-state/CHANGELOG.md create mode 100644 plugins/window-state/Cargo.toml create mode 100644 plugins/window-state/LICENSE.spdx create mode 100644 plugins/window-state/LICENSE_APACHE-2.0 create mode 100644 plugins/window-state/LICENSE_MIT create mode 100644 plugins/window-state/README.md create mode 100644 plugins/window-state/SECURITY.md create mode 100644 plugins/window-state/api-iife.js create mode 100644 plugins/window-state/banner.png create mode 100644 plugins/window-state/build.rs create mode 100644 plugins/window-state/guest-js/index.ts create mode 100644 plugins/window-state/package.json create mode 100644 plugins/window-state/permissions/autogenerated/commands/filename.toml create mode 100644 plugins/window-state/permissions/autogenerated/commands/restore_state.toml create mode 100644 plugins/window-state/permissions/autogenerated/commands/save_window_state.toml create mode 100644 plugins/window-state/permissions/autogenerated/reference.md create mode 100644 plugins/window-state/permissions/default.toml create mode 100644 plugins/window-state/permissions/schemas/schema.json create mode 100644 plugins/window-state/rollup.config.js create mode 100644 plugins/window-state/src/cmd.rs create mode 100644 plugins/window-state/src/lib.rs create mode 100644 plugins/window-state/tsconfig.json create mode 100644 pnpm-workspace.yaml create mode 100644 renovate.json create mode 100644 shared/rollup.config.js create mode 100644 shared/template/Cargo.toml create mode 100644 shared/template/LICENSE.spdx create mode 100644 shared/template/LICENSE_APACHE-2.0 create mode 100644 shared/template/LICENSE_MIT create mode 100644 shared/template/README.md create mode 100644 shared/template/SECURITY.md create mode 100644 shared/template/android/.gitignore create mode 100644 shared/template/android/build.gradle.kts create mode 100644 shared/template/android/consumer-rules.pro create mode 100644 shared/template/android/settings.gradle create mode 100644 shared/template/android/src/androidTest/java/ExampleInstrumentedTest.kt create mode 100644 shared/template/android/src/main/AndroidManifest.xml create mode 100644 shared/template/android/src/main/java/ExamplePlugin.kt create mode 100644 shared/template/android/src/test/java/ExampleUnitTest.kt create mode 100644 shared/template/build.rs create mode 100644 shared/template/guest-js/index.ts create mode 100644 shared/template/ios/.gitignore create mode 100644 shared/template/ios/Package.swift create mode 100644 shared/template/ios/README.md create mode 100644 shared/template/ios/Sources/ExamplePlugin.swift create mode 100644 shared/template/ios/Tests/PluginTests/PluginTests.swift create mode 100644 shared/template/package.json create mode 100644 shared/template/rollup.config.js create mode 100644 shared/template/src/commands.rs create mode 100644 shared/template/src/desktop.rs create mode 100644 shared/template/src/error.rs create mode 100644 shared/template/src/lib.rs create mode 100644 shared/template/src/mobile.rs create mode 100644 shared/template/src/models.rs create mode 100644 shared/template/tsconfig.json create mode 100644 taplo.toml create mode 100644 tsconfig.base.json diff --git a/.cargo/audit.toml b/.cargo/audit.toml new file mode 100644 index 0000000..de84e84 --- /dev/null +++ b/.cargo/audit.toml @@ -0,0 +1,20 @@ +[advisories] +ignore = [ + # rsa Marvin Attack + "RUSTSEC-2023-0071", + # time crate can't be updated in the repo because of MSRV, users are unaffected + "RUSTSEC-2026-0009", + # libflate crates can't be updated in the repo because of MSRV, users are unaffected + "RUSTSEC-2026-0105", + # quick-xml, need an update in plist, can't be updated in the repo because of MSRV, users are unaffected + # Also needs on update in wayland-scanner > arboard > tauri-plugin-clipboard-manager + "RUSTSEC-2026-0194", + # quick-xml, need an update in plist, can't be updated in the repo because of MSRV, users are unaffected + # Also needs on update in wayland-scanner > arboard > tauri-plugin-clipboard-manager + "RUSTSEC-2026-0195", + # quinn-proto, can't be updated in the repo because of MSRV, users are unaffected + "RUSTSEC-2026-0185", + # rkyv is not used, it's brought in by `rust_decimal`'s `rkyv` feature to the lockfile, + # but that feature is not enabled by anything + "RUSTSEC-2026-0235", +] diff --git a/.changes/config.json b/.changes/config.json new file mode 100644 index 0000000..93ed46e --- /dev/null +++ b/.changes/config.json @@ -0,0 +1,355 @@ +{ + "gitSiteUrl": "https://github.com/tauri-apps/plugins-workspace/", + "pkgManagers": { + "javascript": { + "version": true, + "getPublishedVersion": { + "use": "fetch:check", + "options": { + "url": "https://registry.npmjs.com/${ pkg.pkgFile.pkg.name }/${ pkg.pkgFile.version }" + } + }, + "publish": [ + { + "command": "pnpm build", + "dryRunCommand": "pnpm build" + }, + { + "command": "echo '
\n

PNPM Publish

\n\n```'", + "dryRunCommand": true, + "pipe": true + }, + { + "command": "npm publish --provenance --access public", + "dryRunCommand": "npm publish --provenance --access public --dry-run", + "pipe": true + }, + { + "command": "echo '```\n\n
\n'", + "dryRunCommand": true, + "pipe": true + } + ] + }, + "rust": { + "version": true, + "getPublishedVersion": { + "use": "fetch:check", + "options": { + "url": "https://crates.io/api/v1/crates/${ pkg.pkgFile.pkg.package.name }/${ pkg.pkgFile.version }" + } + }, + "publish": [ + { + "command": "echo '
\n

Cargo Publish

\n\n```'", + "dryRunCommand": true, + "pipe": true + }, + { + "command": "cargo publish --no-verify", + "dryRunCommand": "cargo publish --dry-run", + "pipe": true + }, + { + "command": "echo '```\n\n
\n'", + "dryRunCommand": true, + "pipe": true + } + ] + } + }, + "packages": { + "api-example": { + "path": "./examples/api/src-tauri", + "manager": "rust", + "publish": false, + "dependencies": [ + "barcode-scanner", + "biometric", + "log", + "cli", + "clipboard-manager", + "dialog", + "fs", + "global-shortcut", + "opener", + "http", + "nfc", + "notification", + "os", + "process", + "shell", + "store", + "updater", + "geolocation", + "haptics" + ] + }, + "api-example-js": { + "path": "./examples/api", + "manager": "javascript", + "publish": false, + "dependencies": [ + "barcode-scanner-js", + "biometric-js", + "log-js", + "cli-js", + "clipboard-manager-js", + "dialog-js", + "fs-js", + "global-shortcut-js", + "opener-js", + "http-js", + "nfc-js", + "notification-js", + "os-js", + "process-js", + "shell-js", + "store-js", + "updater-js" + ], + "postversion": "pnpm install --no-frozen-lockfile" + }, + "deep-link-example-js": { + "path": "./plugins/deep-link/examples/app", + "manager": "javascript", + "publish": false, + "dependencies": ["deep-link-js"], + "postversion": "pnpm install --no-frozen-lockfile" + }, + "autostart": { + "path": "./plugins/autostart", + "manager": "rust" + }, + "autostart-js": { + "path": "./plugins/autostart", + "manager": "javascript" + }, + "barcode-scanner": { + "path": "./plugins/barcode-scanner", + "manager": "rust" + }, + "barcode-scanner-js": { + "path": "./plugins/barcode-scanner", + "manager": "javascript" + }, + "biometric": { + "path": "./plugins/biometric", + "manager": "rust" + }, + "biometric-js": { + "path": "./plugins/biometric", + "manager": "javascript" + }, + "cli": { + "path": "./plugins/cli", + "manager": "rust" + }, + "cli-js": { + "path": "./plugins/cli", + "manager": "javascript" + }, + "clipboard-manager": { + "path": "./plugins/clipboard-manager", + "manager": "rust" + }, + "clipboard-manager-js": { + "path": "./plugins/clipboard-manager", + "manager": "javascript" + }, + "deep-link": { + "path": "./plugins/deep-link", + "manager": "rust" + }, + "deep-link-js": { + "path": "./plugins/deep-link", + "manager": "javascript" + }, + "fs": { + "path": "./plugins/fs", + "manager": "rust" + }, + "fs-js": { + "path": "./plugins/fs", + "manager": "javascript" + }, + "dialog": { + "path": "./plugins/dialog", + "manager": "rust", + "dependencies": ["fs"] + }, + "dialog-js": { + "path": "./plugins/dialog", + "manager": "javascript", + "dependencies": ["fs-js"] + }, + "geolocation": { + "path": "./plugins/geolocation", + "manager": "rust" + }, + "geolocation-js": { + "path": "./plugins/geolocation", + "manager": "javascript" + }, + "global-shortcut": { + "path": "./plugins/global-shortcut", + "manager": "rust" + }, + "global-shortcut-js": { + "path": "./plugins/global-shortcut", + "manager": "javascript" + }, + "opener": { + "path": "./plugins/opener", + "manager": "rust" + }, + "opener-js": { + "path": "./plugins/opener", + "manager": "javascript" + }, + "haptics": { + "path": "./plugins/haptics", + "manager": "rust" + }, + "haptics-js": { + "path": "./plugins/haptics", + "manager": "javascript" + }, + "http": { + "path": "./plugins/http", + "manager": "rust", + "dependencies": ["fs"] + }, + "http-js": { + "path": "./plugins/http", + "manager": "javascript", + "dependencies": ["fs-js"] + }, + "localhost": { + "path": "./plugins/localhost", + "manager": "rust" + }, + "log": { + "path": "./plugins/log", + "manager": "rust" + }, + "log-js": { + "path": "./plugins/log", + "manager": "javascript" + }, + "nfc": { + "path": "./plugins/nfc", + "manager": "rust" + }, + "nfc-js": { + "path": "./plugins/nfc", + "manager": "javascript" + }, + "notification": { + "path": "./plugins/notification", + "manager": "rust" + }, + "notification-js": { + "path": "./plugins/notification", + "manager": "javascript" + }, + "os": { + "path": "./plugins/os", + "manager": "rust" + }, + "os-js": { + "path": "./plugins/os", + "manager": "javascript" + }, + "persisted-scope": { + "path": "./plugins/persisted-scope", + "manager": "rust", + "dependencies": ["fs"] + }, + "positioner": { + "path": "./plugins/positioner", + "manager": "rust" + }, + "positioner-js": { + "path": "./plugins/positioner", + "manager": "javascript" + }, + "process": { + "path": "./plugins/process", + "manager": "rust" + }, + "process-js": { + "path": "./plugins/process", + "manager": "javascript" + }, + "shell": { + "path": "./plugins/shell", + "manager": "rust" + }, + "shell-js": { + "path": "./plugins/shell", + "manager": "javascript" + }, + "single-instance": { + "path": "./plugins/single-instance", + "manager": "rust", + "dependencies": ["deep-link"] + }, + "sql": { + "path": "./plugins/sql", + "manager": "rust" + }, + "sql-js": { + "path": "./plugins/sql", + "manager": "javascript" + }, + "store": { + "path": "./plugins/store", + "manager": "rust" + }, + "store-js": { + "path": "./plugins/store", + "manager": "javascript" + }, + "stronghold": { + "path": "./plugins/stronghold", + "manager": "rust" + }, + "stronghold-js": { + "path": "./plugins/stronghold", + "manager": "javascript" + }, + "updater": { + "path": "./plugins/updater", + "manager": "rust" + }, + "updater-js": { + "path": "./plugins/updater", + "manager": "javascript" + }, + "upload": { + "path": "./plugins/upload", + "manager": "rust" + }, + "upload-js": { + "path": "./plugins/upload", + "manager": "javascript" + }, + "websocket": { + "path": "./plugins/websocket", + "manager": "rust" + }, + "websocket-js": { + "path": "./plugins/websocket", + "manager": "javascript" + }, + "window-state": { + "path": "./plugins/window-state", + "manager": "rust" + }, + "window-state-js": { + "path": "./plugins/window-state", + "manager": "javascript" + } + } +} diff --git a/.changes/fix-autostart-windows-disable.md b/.changes/fix-autostart-windows-disable.md new file mode 100644 index 0000000..1bae668 --- /dev/null +++ b/.changes/fix-autostart-windows-disable.md @@ -0,0 +1,5 @@ +--- +autostart: patch +--- + +Fixed `disable()` failing on Windows when autostart is already disabled. It now succeeds, as it does on macOS and Linux. diff --git a/.changes/fix-deep-link-linux-unregister.md b/.changes/fix-deep-link-linux-unregister.md new file mode 100644 index 0000000..ab542f7 --- /dev/null +++ b/.changes/fix-deep-link-linux-unregister.md @@ -0,0 +1,5 @@ +--- +deep-link: patch +--- + +Fixed `unregister` on Linux leaving the app as the scheme's handler: it now also removes the scheme from the `MimeType` of the handler's `.desktop` file and refreshes the desktop database, which `xdg-mime` falls back to. It also no longer fails when `mimeapps.list` does not exist. diff --git a/.changes/fix-geo-haptics-specta-skip.md b/.changes/fix-geo-haptics-specta-skip.md new file mode 100644 index 0000000..78f11d5 --- /dev/null +++ b/.changes/fix-geo-haptics-specta-skip.md @@ -0,0 +1,8 @@ +--- +"haptics": patch +"geolocation": patch +"haptics-js": patch +"geolocation-js": patch +--- + +Fixed a build failure when the `specta` feature is enabled: the `Error` type used the `#[serde(skip)]` helper attribute, which recent `specta` versions no longer register. It now uses `#[specta(skip)]`. diff --git a/.changes/fix-notification-mobile-permissions.md b/.changes/fix-notification-mobile-permissions.md new file mode 100644 index 0000000..8a8174a --- /dev/null +++ b/.changes/fix-notification-mobile-permissions.md @@ -0,0 +1,11 @@ +--- +notification: patch +notification-js: patch +--- + +Fixed broken mobile APIs: + +- `channels()` invoked the `listChannels` command, which the `allow-list-channels` permission does not match; it now invokes `list_channels`. +- Unregistering an `onNotificationReceived` or `onAction` listener invokes the `remove_listener` command, which had no permission. It is now declared and part of the `default` permission set (`allow-remove-listener`). +- `cancelAll()` was rejected on Android and iOS, which required the list of notifications to cancel. It now cancels every pending notification. +- `active()` resolved with `{ values: [...] }` instead of an array on Android. diff --git a/.changes/fix-notification-windows-permission.md b/.changes/fix-notification-windows-permission.md new file mode 100644 index 0000000..8dc1a40 --- /dev/null +++ b/.changes/fix-notification-windows-permission.md @@ -0,0 +1,6 @@ +--- +notification: patch +notification-js: patch +--- + +Fixed `isPermissionGranted()` always resolving to `false` on Windows. The initialization script short-circuited the permission check to avoid invoking the backend, but read the permission back before it had been set, so it settled on `denied` instead of `granted`. diff --git a/.changes/global-api-script-reuse-core.md b/.changes/global-api-script-reuse-core.md new file mode 100644 index 0000000..7fd096b --- /dev/null +++ b/.changes/global-api-script-reuse-core.md @@ -0,0 +1,58 @@ +--- +"autostart": patch +"autostart-js": patch +"barcode-scanner": patch +"barcode-scanner-js": patch +"biometric": patch +"biometric-js": patch +"cli": patch +"cli-js": patch +"clipboard-manager": patch +"clipboard-manager-js": patch +"deep-link": patch +"deep-link-js": patch +"dialog": patch +"dialog-js": patch +"fs": patch +"fs-js": patch +"geolocation": patch +"geolocation-js": patch +"global-shortcut": patch +"global-shortcut-js": patch +"haptics": patch +"haptics-js": patch +"http": patch +"http-js": patch +"log": patch +"log-js": patch +"nfc": patch +"nfc-js": patch +"notification": patch +"notification-js": patch +"opener": patch +"opener-js": patch +"os": patch +"os-js": patch +"positioner": patch +"positioner-js": patch +"process": patch +"process-js": patch +"shell": patch +"shell-js": patch +"sql": patch +"sql-js": patch +"store": patch +"store-js": patch +"stronghold": patch +"stronghold-js": patch +"updater": patch +"updater-js": patch +"upload": patch +"upload-js": patch +"websocket": patch +"websocket-js": patch +"window-state": patch +"window-state-js": patch +--- + +The plugin's global API script (used with `app.withGlobalTauri`) now resolves the core API from `window.__TAURI__` instead of bundling its own copy of `@tauri-apps/api`. Values created with the core API are now accepted by plugin APIs in global mode (e.g. an `Image` from `window.__TAURI__.image` passed to `clipboardManager.writeImage`, which previously failed the `instanceof` check against the plugin's private copy), and the script is considerably smaller. diff --git a/.changes/readme.md b/.changes/readme.md new file mode 100644 index 0000000..6d9396b --- /dev/null +++ b/.changes/readme.md @@ -0,0 +1,32 @@ +# Changes + +##### via https://github.com/jbolda/covector + +As you create PRs and make changes that require a version bump, please add a new markdown file in this folder. You do not note the version _number_, but rather the type of bump that you expect: major, minor, or patch. The filename is not important, as long as it is a `.md`, but we recommend that it represents the overall change for organizational purposes. + +When you select the version bump required, you do _not_ need to consider dependencies. Only note the package with the actual change, and any packages that depend on that package will be bumped automatically in the process. + +**Note, that in this repository, even if only the Rust code or only the JavaScript code of a plugin changed, both packages need to be bumped with the same increment!** + +Use the following format: + +```md +--- +"package-a": patch +"package-b": minor:feat +--- + +Change summary goes here +``` + +Summaries do not have a specific character limit, but are text only. These summaries are used within the (future implementation of) changelogs. They will give context to the change and also point back to the original PR if more details and context are needed. + +Changes will be designated as a `major`, `minor` or `patch` as further described in [semver](https://semver.org/). + +Given a version number MAJOR.MINOR.PATCH, increment the: + +- MAJOR version when you make incompatible API changes, +- MINOR version when you add functionality in a backwards compatible manner, and +- PATCH version when you make backwards compatible bug fixes. + +Additional labels for pre-release and build metadata are available as extensions to the MAJOR.MINOR.PATCH format, but will be discussed prior to usage (as extra steps will be necessary in consideration of merging and publishing). diff --git a/.changes/update-docs.md b/.changes/update-docs.md new file mode 100644 index 0000000..1d491b1 --- /dev/null +++ b/.changes/update-docs.md @@ -0,0 +1,61 @@ +--- +"autostart": patch +"barcode-scanner": patch +"biometric": patch +"cli": patch +"clipboard-manager": patch +"deep-link": patch +"dialog": patch +"fs": patch +"global-shortcut": patch +"http": patch +"localhost": patch +"log": patch +"nfc": patch +"notification": patch +"opener": patch +"os": patch +"persisted-scope": patch +"positioner": patch +"process": patch +"shell": patch +"single-instance": patch +"sql": patch +"store": patch +"stronghold": patch +"updater": patch +"upload": patch +"websocket": patch +"window-state": patch +"autostart-js": patch +"barcode-scanner-js": patch +"biometric-js": patch +"cli-js": patch +"clipboard-manager-js": patch +"deep-link-js": patch +"dialog-js": patch +"fs-js": patch +"global-shortcut-js": patch +"http-js": patch +"log-js": patch +"nfc-js": patch +"notification-js": patch +"opener-js": patch +"os-js": patch +"positioner-js": patch +"process-js": patch +"shell-js": patch +"sql-js": patch +"store-js": patch +"stronghold-js": patch +"updater-js": patch +"upload-js": patch +"websocket-js": patch +"window-state-js": patch +"haptics": patch +"haptics-js": patch +"geolocation": patch +"geolocation-js": patch +--- + +Update documentation. diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 0000000..83ba0c4 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,5 @@ +# Order is important; the last matching pattern takes the most precedence. +* @tauri-apps/plugin-maintainers + +# Currently CI/CD for plugins are in heavy flux, and the plugin team manages it themselves. +# .github @tauri-apps/wg-devops diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md new file mode 100644 index 0000000..a2ba06f --- /dev/null +++ b/.github/CONTRIBUTING.md @@ -0,0 +1,73 @@ +# Tauri Contributing Guide + +Hi! We, the maintainers, are really excited that you are interested in contributing to Tauri. Before submitting your contribution though, please make sure to take a moment and read through the [Code of Conduct](CODE_OF_CONDUCT.md), as well as the appropriate section for the contribution you intend to make: + +- [Issue Reporting Guidelines](#issue-reporting-guidelines) +- [Pull Request Guidelines](#pull-request-guidelines) +- [Development Guide](#development-guide) +- [AI Tool Policy](#ai-tool-policy) + +## Issue Reporting Guidelines + +- The issue list of this repo is **exclusively** for bug reports and feature requests. Non-conforming issues will be closed immediately. + +- If you have a question, you can get quick answers from the [Tauri Discord chat](https://discord.com/invite/tauri). + +- Try to search for your issue, it may have already been answered or even fixed in the development branch (`v2`). + +- Check if the issue is reproducible with the latest stable version of Tauri. If you are using a pre-release, please indicate the specific version you are using. + +- It is **required** that you clearly describe the steps necessary to reproduce the issue you are running into. Although we would love to help our users as much as possible, diagnosing issues without clear reproduction steps is extremely time-consuming and simply not sustainable. + +- Use only the minimum amount of code necessary to reproduce the unexpected behavior. A good bug report should isolate specific methods that exhibit unexpected behavior and precisely define how expectations were violated. What did you expect the method or methods to do, and how did the observed behavior differ? The more precisely you isolate the issue, the faster we can investigate. + +- Issues with no clear repro steps will not be triaged. If an issue labeled "need repro" receives no further input from the issue author for more than 5 days, it will be closed. + +- If your issue is resolved but still open, don't hesitate to close it. In case you found a solution by yourself, it could be helpful for others to explain how you fixed it. + +- Most importantly, we beg your patience: the team must balance your request against many other responsibilities — fixing other bugs, answering other questions, new features, new documentation, etc. The issue list is not paid support and we cannot make guarantees about how fast your issue can be resolved. + +## Pull Request Guidelines + +- You have to [sign your commits](https://docs.github.com/en/authentication/managing-commit-signature-verification/signing-commits). + +- It's OK to have multiple small commits as you work on the PR - we will let GitHub automatically squash it before merging. + +- If adding new feature: + - Provide convincing reason to add this feature. Ideally you should open a suggestion issue first and have it greenlighted before working on it. + +- If fixing a bug: + - If you are resolving a special issue, add `(fix: #xxxx[,#xxx])` (#xxxx is the issue id) in your PR title for a better release log, e.g. `fix: update entities encoding/decoding (fix #3899)`. + - Provide detailed description of the bug in the PR, or link to an issue that does. + +- If the PR is meant to be released, follow the instructions in `.changes/readme.md` to log your changes. ie. [readme.md](https://github.com/tauri-apps/plugins-workspace/blob/v2/.changes/readme.md) + +## Development Guide + +**NOTE: If you have any question don't hesitate to ask in our Discord server. We try to keep this guide to up guide, but if something doesn't work let us know.** + +### General Setup + +First, [join our Discord server](https://discord.com/invite/tauri) and let us know that you want to contribute. This way we can point you in the right direction and help ensure your contribution will be as helpful as possible. + +To set up your machine for development, follow the [Tauri setup guide](https://v2.tauri.app/start/prerequisites/) to get all the tools you need to develop Tauri apps. The only additional tool you may need is [PNPM](https://pnpm.io/). + +Next, [fork](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/fork-a-repo) and clone [this repository](https://github.com/tauri-apps/plugins-workspace/tree/v2). + +### Developing and testing + +The easiest way to test your changes is to use the [example app](https://github.com/tauri-apps/plugins-workspace/tree/v2/examples/api) example app. It automatically rebuilds and uses your local copy of the plugins. To run it follow the instructions inside [README.md](https://github.com/tauri-apps/plugins-workspace/blob/v2/examples/api/README.md). + +To test local changes against your own application simply point the plugin create to your local repository, for example: + +`tauri-plugin-sample = { path = "path/to/local/tauri-plugin-sample/" }` + +## AI Tool Policy + +It takes a lot of time to review a Pull Request while it's very easy to make a nonsensical but plausible looking one using AI tools. +It is unfair for other contributors and the reviewers to spend much of the time dealing with this, hence these rules: + +1. Review and test all LLM-generated content before submitting, you're the one responsible for it, not the AI. +2. Don't use AI to respond to review comments (except for translations). + +We will close the Pull Request with a `ai-slop` tag if you failed to do so. diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml new file mode 100644 index 0000000..2e13004 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -0,0 +1,63 @@ +# Copyright 2019-2023 Tauri Programme within The Commons Conservancy +# SPDX-License-Identifier: Apache-2.0 +# SPDX-License-Identifier: MIT + +name: 🐞 Bug Report +description: Report a bug +labels: ['type: bug'] + +body: + - type: markdown + attributes: + value: | + ## First of all + 1. Please search for [existing issues](https://github.com/tauri-apps/plugins-workspace/issues?q=is%3Aissue) about this problem first. + 2. Make sure `rustc` and all relevant Tauri packages are up to date. + 3. Make sure it's an issue with a tauri plugin and not something else you are using. + 4. Remember to follow our community guidelines and be friendly. + + - type: textarea + id: description + attributes: + label: Describe the bug + description: A clear description of what the bug is. Include screenshots if applicable. + placeholder: Bug description + validations: + required: true + + - type: textarea + id: reproduction + attributes: + label: Reproduction + description: A link to a reproduction repo or steps to reproduce the behaviour. + placeholder: | + Please provide a minimal reproduction or steps to reproduce, see this guide https://stackoverflow.com/help/minimal-reproducible-example + Why reproduction is required? see this article https://antfu.me/posts/why-reproductions-are-required + + - type: textarea + id: expected-behavior + attributes: + label: Expected behavior + description: A clear description of what you expected to happen. + + - type: textarea + id: info + attributes: + label: Full `tauri info` output + description: 'Output of `npm run tauri info` or `cargo tauri info`. Issues without this will be closed!' + render: text + validations: + required: true + + - type: textarea + id: logs + attributes: + label: Stack trace + description: A stack trace or ANY error messages you may see. + render: text + + - type: textarea + id: context + attributes: + label: Additional context + description: Add any other context about the problem here. For example a link to a Discord discussion. diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000..f19ffc7 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,12 @@ +# Copyright 2019-2023 Tauri Programme within The Commons Conservancy +# SPDX-License-Identifier: Apache-2.0 +# SPDX-License-Identifier: MIT + +#blank_issues_enabled: false +contact_links: + - name: 💡 Request a new plugin + url: https://github.com/orgs/tauri-apps/discussions/new?category=plugin-requests + about: Propose a new Plugin to the community or the Tauri org. + - name: 💬 Discord Chat + url: https://discord.com/invite/tauri + about: Ask questions and talk to other Tauri users and the maintainers diff --git a/.github/ISSUE_TEMPLATE/docs_report.md b/.github/ISSUE_TEMPLATE/docs_report.md new file mode 100644 index 0000000..267c967 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/docs_report.md @@ -0,0 +1,5 @@ +--- +name: 📚 Docs Report +about: Create a report to help us improve the docs +labels: 'type: documentation' +--- diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml new file mode 100644 index 0000000..3e9f403 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -0,0 +1,46 @@ +# Copyright 2019-2023 Tauri Programme within The Commons Conservancy +# SPDX-License-Identifier: Apache-2.0 +# SPDX-License-Identifier: MIT + +name: 💡 Feature Request +description: Request a feature or enhancement for an existing plugin +labels: ['type: feature request'] + +body: + - type: markdown + attributes: + value: | + ## First of all + - Please search for [existing issues](https://github.com/tauri-apps/plugins-workspace/issues?q=is%3Aissue) for this request first. + - Only requests for plugins that exist in this repo are allowed. + - You can request new plugins [here](https://github.com/orgs/tauri-apps/discussions/new?category=plugin-requests) + + - type: textarea + id: problem + attributes: + label: Describe the problem + description: A clear description of the problem this feature would solve + placeholder: "I'm always frustrated when..." + validations: + required: true + + - type: textarea + id: solution + attributes: + label: "Describe the solution you'd like" + description: A clear description of what change you would like + placeholder: 'I would like to...' + validations: + required: true + + - type: textarea + id: alternatives + attributes: + label: Alternatives considered + description: "Any alternative solutions you've considered" + + - type: textarea + id: context + attributes: + label: Additional context + description: Add any other context about the problem here. diff --git a/.github/sponsors/crabnebula.svg b/.github/sponsors/crabnebula.svg new file mode 100644 index 0000000..40e2413 --- /dev/null +++ b/.github/sponsors/crabnebula.svg @@ -0,0 +1,31 @@ + \ No newline at end of file diff --git a/.github/sponsors/rescue.png b/.github/sponsors/rescue.png new file mode 100644 index 0000000000000000000000000000000000000000..2b5916f408daacf0ae1938d2e9dc7511ceffdbbf GIT binary patch literal 8071 zcmZ8`by$?$7cHIA0@B?KAt6Xf_keT`ozguZAT3=2qI5`i!_W=VC5|Rym9w_|qsK7HX?^PaSpwI*0Zyvs?|Bd0R$OB6a9tb@K zkF5V-4<)$Tqt0Lbza=%eCR`By2tTmktD4Zgy2!oKqlbs>zh}V>Jis0;!IKYWgzja; z|26ZkD_OCByukxC(FZzQ@V^uP9syrf;5v^!|F@S1OG5XuqW{?VyM6uGmmEC#X!39J zAFGd+;7%0&9n=Hn5hnl0g2Uhq!lwsk$^TUldhnzAFAN?$#PHr9%{<8d-Tn<805%V_*H~b*@IC&+JhXFpY z{>SIvLm%e$pXW#X|7Yf}$iuJpZ}o2mzWvX}L+M|8@EGp!^^5;$>XE4OKY;(seLVVM z`=9v#3wuz8SN~P}yM4$A-NOap>jUvGFpaB92y=So0y!Qott0ywz#ysvbwgm zw!X3PeQOK4y|cG}aCmrpeDd=Y_Ur8Y;_COG>zmuVJL~A!Qv?KxSBf%{T0Tp&#&)lW z2UDFe$X}~VKI!)F7dTG*DRbQWECs(wVZin|7c1Foxk!tZv})^3LK*7j&FA(#&(2CD z31a9GPwA1O(82}@jb5o^Ac0*dd~lmxPL9Oqw2kLhR(=Oq=C+NC8K2L29b{-ylKH9_ z9nwJUMhWs%Y0?TM(Kj{fo>L@JwoQ0fHMLA~m$1ZOTi~evU)h9D6I^U%wUY^`g zXQ^Di$al9gs#iEJWJ^9OvCreQ8ju()HkaI3;M_n@8uikhK=`9jP{^+>g8>ome

fjuOLaq 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 literal 0 HcmV?d00001 diff --git a/.github/sync-to-mirrors.sh b/.github/sync-to-mirrors.sh new file mode 100755 index 0000000..4baf55d --- /dev/null +++ b/.github/sync-to-mirrors.sh @@ -0,0 +1,136 @@ +#!/bin/bash +# Copyright 2019-2023 Tauri Programme within The Commons Conservancy +# SPDX-License-Identifier: Apache-2.0 +# SPDX-License-Identifier: MIT + +## Environment used by this script: +# +# Current directory must be within the repo being mirrored from, so the commit message can be read. +# +# Required: +# - BUILD_BASE: Path to the build directory, which contains "mirrors.txt" and directories for each repo to mirror to. +# - GITHUB_ACTOR: GitHub username for the commit being mirrored. +# - GITHUB_REF: Git ref being mirrored from, e.g. "refs/heads/main". Must begin with "refs/heads/". +# +# Other: +# - API_TOKEN_GITHUB: Personal access token to use when accessing GitHub. +# - CI: If unset or empty, the commits will be prepared but the actual push will not happen. +# - COMMIT_MESSAGE: Commit message to use for the mirror commits. Will be read from HEAD in `SOURCE_DIR` if not specified. +# - GITHUB_REPOSITORY: GH repository, used in the commit message if `COMMIT_MESSAGE` is not specified. +# - GITHUB_RUN_ID: GH Actions run ID, used in the commit message if `COMMIT_MESSAGE` is not specified. +# - GITHUB_SHA: Head SHA1 from which to fetch the commit message for the commit being mirrored. HEAD will be assumed if not specified. +# - SOURCE_DIR: Source directory, used when `COMMIT_MESSAGE` is not specified. + +# Halt on error +set -eo pipefail + +if [[ -n "$CI" ]]; then + export GIT_AUTHOR_NAME="tauri-bot" + export GIT_AUTHOR_EMAIL="tauri-bot@users.noreply.github.com" + export GIT_COMMITTER_NAME="tauri-bot" + export GIT_COMMITTER_EMAIL="tauri-bot@users.noreply.github.com" +fi + +if [[ -z "$BUILD_BASE" ]]; then + echo "::error::BUILD_BASE must be set" + exit 1 +elif [[ ! -d "$BUILD_BASE" ]]; then + echo "::error::$BUILD_BASE does not exist or is not a directory" + exit 1 +fi + +if [[ -z "$COMMIT_MESSAGE" ]]; then + MONOREPO_COMMIT_MESSAGE=$(cd "${SOURCE_DIR:-.}" && git show -s --format=%B $GITHUB_SHA) + COMMIT_MESSAGE=$( printf "%s\n\nCommitted via a GitHub action: https://github.com/%s/actions/runs/%s" "$MONOREPO_COMMIT_MESSAGE" "$GITHUB_REPOSITORY" "$GITHUB_RUN_ID" ) +fi +COMMIT_ACTOR="${GITHUB_ACTOR} <${GITHUB_ACTOR}@users.noreply.github.com>" +COMMIT_AUTHOR=$(cd "${SOURCE_DIR:-.}" &&git show -s --format="%an <%ae>" $GITHUB_SHA) + +if [[ "$GITHUB_REF" =~ ^refs/heads/ ]]; then + BRANCH=${GITHUB_REF#refs/heads/} +else + echo "::error::Could not determine branch name from $GITHUB_REF" + exit 1 +fi + +if [[ ! -f "$BUILD_BASE/mirrors.txt" ]]; then + echo "::error::File $BUILD_BASE/mirrors.txt does not exist or is not a file" + exit 1 +elif [[ ! -s "$BUILD_BASE/mirrors.txt" ]]; then + echo "Nothing to do, $BUILD_BASE/mirrors.txt is empty." + exit 0 +fi + +# : > "$BUILD_BASE/changes.diff" + +# Collect tags of current commit +readarray -t COMMIT_TAGS < <(git tag --points-at HEAD) + +EXIT=0 +while read -r PLUGIN_NAME; do + printf "\n\n\e[7m Mirror: %s \e[0m\n" "$PLUGIN_NAME" + CLONE_DIR="${BUILD_BASE}/${PLUGIN_NAME}" + cd "${CLONE_DIR}" + + # Initialize the directory as a git repo, and set the remote + git init -b "$BRANCH" . + git remote add origin "https://github.com/tauri-apps/tauri-plugin-${PLUGIN_NAME}" + if [[ -n "$API_TOKEN_GITHUB" ]]; then + git config --local http.https://github.com/.extraheader "AUTHORIZATION: basic $(printf "x-access-token:%s" "$API_TOKEN_GITHUB" | base64)" + fi + + # Check if a remote exists for that mirror. + if ! git ls-remote -h origin >/dev/null 2>&1; then + echo "::error::Mirror repo for ${PLUGIN_NAME} does not exist." + echo "Skipping." + EXIT=1 + continue + fi + + echo "::group::Fetching ${PLUGIN_NAME}" + FORCE_COMMIT= + if git -c protocol.version=2 fetch --no-tags --prune --progress --no-recurse-submodules --depth=1 origin "$BRANCH"; then + git reset --soft FETCH_HEAD + echo "Fetched revision $(git rev-parse HEAD)" + elif [[ -n "$DEFAULT_BRANCH" ]] && git -c protocol.version=2 fetch --no-tags --prune --progress --no-recurse-submodules --depth=1 origin "$DEFAULT_BRANCH"; then + FORCE_COMMIT=--allow-empty + git reset --soft FETCH_HEAD + echo "Fetched revision $(git rev-parse HEAD)" + else + echo "Failed to find a branch to branch from, just creating an empty one." + FORCE_COMMIT=--allow-empty + fi + git add -A + echo "::endgroup::" + + if [[ -n "$FORCE_COMMIT" || -n "$(git status --porcelain)" ]]; then + echo "Committing to $PLUGIN_NAME" + GIT_CLI_COMMIT_MESSAGE=$( printf "%s \n\nCo-authored-by: %s" "$COMMIT_MESSAGE" "$COMMIT_ACTOR" ) + if git commit $FORCE_COMMIT --author="${COMMIT_AUTHOR}" -m "${GIT_CLI_COMMIT_MESSAGE}" && + { [[ -z "$CI" ]] || git push origin "$BRANCH"; } # Only do the actual push from the GitHub Action + then + # echo "$BUILD_BASE/changes.diff" + # git show --pretty= --src-prefix="a/$PLUGIN_NAME/" --dst-prefix="b/$PLUGIN_NAME/" >> "$BUILD_BASE/changes.diff" + echo "https://github.com/tauri-apps/tauri-plugin-$PLUGIN_NAME/commit/$(git rev-parse HEAD)" + + # Add new tags + for FULL_TAG in "${COMMIT_TAGS[@]}"; do + if [[ "$FULL_TAG" =~ ^"$PLUGIN_NAME-js-v" ]]; then + TAG_NAME="${FULL_TAG#"$PLUGIN_NAME-js-"}" + echo "Creating tag $TAG_NAME" + git tag "${TAG_NAME}" -m "${GIT_CLI_COMMIT_MESSAGE}" + git push origin "${TAG_NAME}" + fi + done + + echo "Completed $PLUGIN_NAME" + else + echo "::error::Commit of ${PLUGIN_NAME} failed" + EXIT=1 + fi + else + echo "No changes, skipping $PLUGIN_NAME" + fi +done < "$BUILD_BASE/mirrors.txt" + +exit $EXIT diff --git a/.github/workflows/audit-javascript.yml b/.github/workflows/audit-javascript.yml new file mode 100644 index 0000000..151d913 --- /dev/null +++ b/.github/workflows/audit-javascript.yml @@ -0,0 +1,49 @@ +# Copyright 2019-2023 Tauri Programme within The Commons Conservancy +# SPDX-License-Identifier: Apache-2.0 +# SPDX-License-Identifier: MIT + +name: Audit JavaScript + +on: + workflow_dispatch: + schedule: + - cron: '0 0 * * *' + push: + branches: + - v1 + - v2 + paths: + - '.github/workflows/audit-javascript.yml' + - '**/pnpm-lock.yaml' + - '**/package.json' + pull_request: + branches: + - v1 + - v2 + paths: + - '.github/workflows/audit-javascript.yml' + - '**/pnpm-lock.yaml' + - '**/package.json' + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +permissions: + contents: read + checks: write + +jobs: + audit-js: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + - uses: actions/setup-node@v7 + with: + node-version: 'lts/*' + - uses: pnpm/action-setup@v6 + with: + run_install: true + cache: true # the pnpm store, keyed by OS and lockfile hash and shared by every workflow + - name: audit + run: pnpm audit diff --git a/.github/workflows/audit-rust.yml b/.github/workflows/audit-rust.yml new file mode 100644 index 0000000..cba74ae --- /dev/null +++ b/.github/workflows/audit-rust.yml @@ -0,0 +1,45 @@ +# Copyright 2019-2023 Tauri Programme within The Commons Conservancy +# SPDX-License-Identifier: Apache-2.0 +# SPDX-License-Identifier: MIT + +name: Audit Rust + +on: + workflow_dispatch: + schedule: + - cron: '0 0 * * *' + push: + branches: + - v1 + - v2 + paths: + - '.github/workflows/audit-rust.yml' + - '**/Cargo.lock' + - '**/Cargo.toml' + pull_request: + branches: + - v1 + - v2 + paths: + - '.github/workflows/audit-rust.yml' + - '**/Cargo.lock' + - '**/Cargo.toml' + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +permissions: + contents: read + checks: write + +jobs: + audit-rust: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + - uses: rustsec/audit-check@v2 + with: + token: ${{ secrets.GITHUB_TOKEN }} + # https://github.com/tauri-apps/plugins-workspace/issues/774 + ignore: ${{ github.event_name != 'schedule' && 'RUSTSEC-2023-0071' || '' }} diff --git a/.github/workflows/check-change-files.yml b/.github/workflows/check-change-files.yml new file mode 100644 index 0000000..e6f4db1 --- /dev/null +++ b/.github/workflows/check-change-files.yml @@ -0,0 +1,47 @@ +# Copyright 2019-2023 Tauri Programme within The Commons Conservancy +# SPDX-License-Identifier: Apache-2.0 +# SPDX-License-Identifier: MIT + +name: check change files + +on: + pull_request: + paths: + - '.changes/*.md' + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +permissions: + contents: read + +jobs: + check: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + + - name: check change files end with .md + run: | + for file in .changes/* + do + if [[ ! "$file" =~ \.(md|json)$ ]]; then + echo ".changes directory should only contain files that end with .md" + echo "found an invalid file in .changes directory:" + echo "$file" + exit 1 + fi + done + + - uses: dorny/paths-filter@v3 + id: filter + with: + list-files: shell + filters: | + changes: + - added|modified: '.changes/*.md' + + - name: check + run: node ./.scripts/ci/check-change-files.js ${{ steps.filter.outputs.changes_files }} + if: ${{ steps.filter.outputs.changes == 'true' }} diff --git a/.github/workflows/check-generated-files.yml b/.github/workflows/check-generated-files.yml new file mode 100644 index 0000000..11f40d1 --- /dev/null +++ b/.github/workflows/check-generated-files.yml @@ -0,0 +1,175 @@ +# Copyright 2019-2023 Tauri Programme within The Commons Conservancy +# SPDX-License-Identifier: Apache-2.0 +# SPDX-License-Identifier: MIT + +name: check generated files + +on: + pull_request: + paths: + - '.github/workflows/check-generated-files.yml' + - pnpm-lock.yaml + - '**/guest-js/**' + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +permissions: + contents: read + +jobs: + changes: + runs-on: ubuntu-latest + outputs: + packages: ${{ steps.filter.outputs.changes }} + steps: + - uses: actions/checkout@v7 + - uses: dorny/paths-filter@v2 + id: filter + with: + filters: | + autostart: + - .github/workflows/check-generated-files.yml + - pnpm-lock.yaml + - plugins/autostart/guest-js/** + - plugins/autostart/src/api-iife.js + cli: + - .github/workflows/check-generated-files.yml + - pnpm-lock.yaml + - plugins/cli/guest-js/** + - plugins/cli/src/api-iife.js + clipboard-manager: + - .github/workflows/check-generated-files.yml + - pnpm-lock.yaml + - plugins/clipboard-manager/guest-js/** + - plugins/clipboard-manager/src/api-iife.js + dialog: + - .github/workflows/check-generated-files.yml + - pnpm-lock.yaml + - plugins/dialog/guest-js/** + - plugins/dialog/src/api-iife.js + fs: + - .github/workflows/check-generated-files.yml + - pnpm-lock.yaml + - plugins/fs/guest-js/** + - plugins/fs/src/api-iife.js + geolocation: + - .github/workflows/check-generated-files.yml + - pnpm-lock.yaml + - plugins/geolocation/guest-js/** + - plugins/geolocation/src/api-iife.js + global-shortcut: + - .github/workflows/check-generated-files.yml + - pnpm-lock.yaml + - plugins/global-shortcut/guest-js/** + - plugins/global-shortcut/src/api-iife.js + opener: + - .github/workflows/check-generated-files.yml + - pnpm-lock.yaml + - plugins/opener/guest-js/** + - plugins/opener/src/api-iife.js + haptics: + - .github/workflows/check-generated-files.yml + - pnpm-lock.yaml + - plugins/haptics/guest-js/** + - plugins/haptics/src/api-iife.js + http: + - .github/workflows/check-generated-files.yml + - pnpm-lock.yaml + - plugins/http/guest-js/** + - plugins/http/src/api-iife.js + log: + - .github/workflows/check-generated-files.yml + - pnpm-lock.yaml + - plugins/log/guest-js/** + - plugins/log/src/api-iife.js + notification: + - .github/workflows/check-generated-files.yml + - pnpm-lock.yaml + - plugins/notification/guest-js/** + - plugins/notification/src/api-iife.js + os: + - .github/workflows/check-generated-files.yml + - pnpm-lock.yaml + - plugins/os/guest-js/** + - plugins/os/src/api-iife.js + positioner: + - .github/workflows/check-generated-files.yml + - pnpm-lock.yaml + - plugins/positioner/guest-js/** + - plugins/positioner/src/api-iife.js + process: + - .github/workflows/check-generated-files.yml + - pnpm-lock.yaml + - plugins/process/guest-js/** + - plugins/process/src/api-iife.js + shell: + - .github/workflows/check-generated-files.yml + - pnpm-lock.yaml + - plugins/shell/guest-js/** + - plugins/shell/src/api-iife.js + sql: + - .github/workflows/check-generated-files.yml + - pnpm-lock.yaml + - plugins/sql/guest-js/** + - plugins/sql/src/api-iife.js + store: + - .github/workflows/check-generated-files.yml + - pnpm-lock.yaml + - plugins/store/guest-js/** + - plugins/store/src/api-iife.js + stronghold: + - .github/workflows/check-generated-files.yml + - pnpm-lock.yaml + - plugins/stronghold/guest-js/** + - plugins/stronghold/src/api-iife.js + updater: + - .github/workflows/check-generated-files.yml + - pnpm-lock.yaml + - plugins/updater/guest-js/** + - plugins/updater/src/api-iife.js + upload: + - .github/workflows/check-generated-files.yml + - pnpm-lock.yaml + - plugins/upload/guest-js/** + - plugins/upload/src/api-iife.js + websocket: + - .github/workflows/check-generated-files.yml + - pnpm-lock.yaml + - plugins/websocket/guest-js/** + - plugins/websocket/src/api-iife.js + window-state: + - .github/workflows/check-generated-files.yml + - pnpm-lock.yaml + - plugins/window-state/guest-js/** + - plugins/window-state/src/api-iife.js + + test: + needs: changes + if: ${{ needs.changes.outputs.packages != '[]' && needs.changes.outputs.packages != '' }} + strategy: + fail-fast: false + matrix: + package: ${{ fromJSON(needs.changes.outputs.packages) }} + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v7 + + - uses: actions/setup-node@v7 + with: + node-version: 'lts/*' + - uses: pnpm/action-setup@v6 + with: + run_install: true + cache: true # the pnpm store, keyed by OS and lockfile hash and shared by every workflow + + - name: build api + working-directory: plugins/${{ matrix.package }} + run: pnpm install && pnpm build + + - name: check diff + run: | + ./.scripts/ci/has-diff.sh diff --git a/.github/workflows/check-license-header.yml b/.github/workflows/check-license-header.yml new file mode 100644 index 0000000..0100d13 --- /dev/null +++ b/.github/workflows/check-license-header.yml @@ -0,0 +1,31 @@ +# Copyright 2019-2023 Tauri Programme within The Commons Conservancy +# SPDX-License-Identifier: Apache-2.0 +# SPDX-License-Identifier: MIT + +name: check license header + +on: + pull_request: + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +permissions: + contents: read + +jobs: + check: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + - uses: dorny/paths-filter@v2 + id: filter + with: + list-files: shell + filters: | + added: + - added: '**' + - name: check header license on new files + if: ${{ steps.filter.outputs.added == 'true' }} + run: node .scripts/ci/check-license-header.js ${{ steps.filter.outputs.added_files }} diff --git a/.github/workflows/covector-comment-on-fork.yml b/.github/workflows/covector-comment-on-fork.yml new file mode 100644 index 0000000..494a534 --- /dev/null +++ b/.github/workflows/covector-comment-on-fork.yml @@ -0,0 +1,30 @@ +# Copyright 2019-2023 Tauri Programme within The Commons Conservancy +# SPDX-License-Identifier: Apache-2.0 +# SPDX-License-Identifier: MIT + +name: covector comment +on: + workflow_run: + workflows: [covector status] # the `name` of the workflow run on `pull_request` running `status` with `comment: true` + types: + - completed + +# note all other permissions are set to none if not specified +# and these set the permissions for `secrets.GITHUB_TOKEN` +permissions: + # to read the action artifacts on `covector status` workflows + actions: read + # to write the comment + pull-requests: write + +jobs: + download: + runs-on: ubuntu-latest + if: github.event.workflow_run.conclusion == 'success' && + (github.event.workflow_run.head_repository.full_name != github.repository || github.actor == 'dependabot[bot]') + steps: + - name: covector status + uses: jbolda/covector/packages/action@covector-v0 + with: + token: ${{ secrets.GITHUB_TOKEN }} + command: 'status' diff --git a/.github/workflows/covector-status.yml b/.github/workflows/covector-status.yml new file mode 100644 index 0000000..0eced3a --- /dev/null +++ b/.github/workflows/covector-status.yml @@ -0,0 +1,25 @@ +# Copyright 2019-2023 Tauri Programme within The Commons Conservancy +# SPDX-License-Identifier: Apache-2.0 +# SPDX-License-Identifier: MIT + +name: covector status +on: [pull_request] + +permissions: + contents: read + +jobs: + covector: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v7 + with: + fetch-depth: 0 # required for use of git history + - name: covector status + uses: jbolda/covector/packages/action@covector-v0 + id: covector + with: + command: 'status' + token: ${{ secrets.GITHUB_TOKEN }} + comment: true diff --git a/.github/workflows/covector-version-or-publish.yml b/.github/workflows/covector-version-or-publish.yml new file mode 100644 index 0000000..ca563b5 --- /dev/null +++ b/.github/workflows/covector-version-or-publish.yml @@ -0,0 +1,84 @@ +# Copyright 2019-2023 Tauri Programme within The Commons Conservancy +# SPDX-License-Identifier: Apache-2.0 +# SPDX-License-Identifier: MIT + +name: version or publish + +on: + push: + branches: + - v1 + - v2 + +permissions: + # required for oidc token + id-token: write + # required to create the GitHub Release + contents: write + # required for creating the Version Packages Release + pull-requests: write + +jobs: + version-or-publish: + runs-on: ubuntu-latest + timeout-minutes: 65 + outputs: + change: ${{ steps.covector.outputs.change }} + commandRan: ${{ steps.covector.outputs.commandRan }} + successfulPublish: ${{ steps.covector.outputs.successfulPublish }} + + steps: + - uses: actions/checkout@v7 + with: + fetch-depth: 0 # required for use of git history + + - uses: actions/setup-node@v7 + with: + node-version: 'lts/*' + registry-url: 'https://registry.npmjs.org' + + - uses: pnpm/action-setup@v6 + with: + run_install: true + + - name: cargo login + run: cargo login ${{ secrets.ORG_CRATES_IO_TOKEN }} + + - name: git config + run: | + git config --global user.name "${{ github.event.pusher.name }}" + git config --global user.email "${{ github.event.pusher.email }}" + + - name: Setup target dir on /mnt + # This directory has a larger partition size + run: | + sudo mkdir /mnt/target + WORKSPACE_OWNER="$(stat -c '%U:%G' "${GITHUB_WORKSPACE}")" + sudo chown -R "${WORKSPACE_OWNER}" /mnt/target + + - name: covector version or publish (publish when no change files present) + uses: jbolda/covector/packages/action@covector-v0 + id: covector + env: + CARGO_TARGET_DIR: /mnt/target + with: + token: ${{ secrets.GITHUB_TOKEN }} + command: 'version-or-publish' + createRelease: true + recognizeContributors: true + + - name: Sync Cargo.lock + if: steps.covector.outputs.commandRan == 'version' + run: cargo tree --depth 0 + + - name: Create Pull Request With Versions Bumped + id: cpr + uses: peter-evans/create-pull-request@v8.1.1 + if: steps.covector.outputs.commandRan == 'version' + with: + title: 'Publish New Versions (${{ github.ref_name }})' + commit-message: 'publish new versions' + labels: 'version updates' + branch: 'ci/release-${{ github.ref_name }}' + body: ${{ steps.covector.outputs.change }} + sign-commits: true diff --git a/.github/workflows/fmt.yml b/.github/workflows/fmt.yml new file mode 100644 index 0000000..bc23cdc --- /dev/null +++ b/.github/workflows/fmt.yml @@ -0,0 +1,55 @@ +# Copyright 2019-2023 Tauri Programme within The Commons Conservancy +# SPDX-License-Identifier: Apache-2.0 +# SPDX-License-Identifier: MIT + +name: check formatting + +on: + pull_request: + +permissions: + contents: read + +jobs: + rustfmt: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v7 + + - name: install Rust stable and rustfmt + uses: dtolnay/rust-toolchain@stable + with: + components: rustfmt + + - name: run cargo fmt + run: cargo fmt --all -- --check + + prettier: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + - uses: actions/setup-node@v7 + with: + node-version: 'lts/*' + - uses: pnpm/action-setup@v6 + with: + run_install: true + cache: true # the pnpm store, keyed by OS and lockfile hash and shared by every workflow + - run: pnpm format:check + + taplo: + name: taplo (.toml files) + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + + - name: install Rust stable + uses: dtolnay/rust-toolchain@stable + + - name: install taplo-cli + uses: taiki-e/install-action@v2 + with: + tool: taplo-cli + + - run: taplo fmt --check --diff diff --git a/.github/workflows/integration-tests.yml b/.github/workflows/integration-tests.yml new file mode 100644 index 0000000..27993d2 --- /dev/null +++ b/.github/workflows/integration-tests.yml @@ -0,0 +1,70 @@ +# Copyright 2019-2023 Tauri Programme within The Commons Conservancy +# SPDX-License-Identifier: Apache-2.0 +# SPDX-License-Identifier: MIT + +name: integration tests + +on: + workflow_dispatch: + push: + branches: + - v1 + - v2 + paths: + - '.github/workflows/integration-tests.yml' + - 'plugins/updater/src/**' + - 'plugins/updater/tests/**' + pull_request: + branches: + - v1 + - v2 + paths: + - '.github/workflows/integration-tests.yml' + - 'plugins/updater/src/**' + - 'plugins/updater/tests/**' + +permissions: + contents: read + +env: + CARGO_PROFILE_DEV_DEBUG: 0 # no debuginfo: faster builds and a much smaller target dir to cache + +jobs: + run-integration-tests: + runs-on: ${{ matrix.platform }} + + strategy: + fail-fast: false + matrix: + platform: [ubuntu-22.04, macos-latest, windows-latest] + + steps: + - uses: actions/checkout@v7 + with: + fetch-depth: 0 + + - name: install stable + uses: dtolnay/rust-toolchain@stable + + - name: install Linux dependencies + if: matrix.platform == 'ubuntu-22.04' + run: | + sudo apt-get update + sudo apt-get install -y webkit2gtk-4.0 libwebkit2gtk-4.1-dev libayatana-appindicator3-dev libfuse2 rpm + + - uses: Swatinem/rust-cache@v2 + with: + # Only `v2`/`v3` write to the cache: an entry saved from a pull request lives on its + # merge ref, where nothing else can restore it, and every write evicts an entry + # that other runs would have hit (10 GB repository limit, LRU eviction). + save-if: ${{ github.ref == 'refs/heads/v2' || github.ref == 'refs/heads/v3' }} + + - name: install Tauri CLI + # `--debug --target-dir target`: a dev-profile build in the workspace target dir reuses + # the cached dependencies (shared with the test build below) instead of a from-scratch + # release build in a temporary dir. `--locked` keeps the dependency versions stable + # between runs, so the cached artifacts stay valid. + run: cargo install tauri-cli --git https://github.com/tauri-apps/tauri --branch dev --locked --debug --target-dir target + + - name: run integration tests + run: cargo test --test '*' -- --ignored diff --git a/.github/workflows/lint-javascript.yml b/.github/workflows/lint-javascript.yml new file mode 100644 index 0000000..c6dbce4 --- /dev/null +++ b/.github/workflows/lint-javascript.yml @@ -0,0 +1,59 @@ +# Copyright 2019-2023 Tauri Programme within The Commons Conservancy +# SPDX-License-Identifier: Apache-2.0 +# SPDX-License-Identifier: MIT + +name: Lint JavaScript + +on: + push: + branches: + - v1 + - v2 + paths: + - '.github/workflows/lint-javascript.yml' + - 'plugins/*/guest-js/**' + - 'packages/**' + - '.eslintignore' + - '.eslintrc.json' + - '.prettierignore' + - '**/package.json' + pull_request: + branches: + - v1 + - v2 + paths: + - '.github/workflows/lint-javascript.yml' + - 'plugins/*/guest-js/**' + - 'packages/**' + - '.eslintignore' + - '.eslintrc.json' + - '.prettierignore' + - '**/package.json' + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +permissions: + contents: read + +jobs: + eslint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + - uses: actions/setup-node@v7 + with: + node-version: 'lts/*' + - uses: pnpm/action-setup@v6 + with: + run_install: true + cache: true # the pnpm store, keyed by OS and lockfile hash and shared by every workflow + # the type-checked lint rules (and packages/api-e2e) resolve the plugins' + # types from their dist-js build output + - name: build + run: pnpm build + - name: eslint + run: pnpm lint + - name: ts:check + run: pnpm ts:check diff --git a/.github/workflows/lint-rust.yml b/.github/workflows/lint-rust.yml new file mode 100644 index 0000000..ede7bf6 --- /dev/null +++ b/.github/workflows/lint-rust.yml @@ -0,0 +1,183 @@ +# Copyright 2019-2023 Tauri Programme within The Commons Conservancy +# SPDX-License-Identifier: Apache-2.0 +# SPDX-License-Identifier: MIT + +name: Lint Rust + +on: + push: + branches: + - v1 + - v2 + paths: + - '.github/workflows/lint-rust.yml' + - 'plugins/*/src/**' + - '!plugins/*/src/api-iife.js' + - '**/Cargo.toml' + pull_request: + branches: + - v1 + - v2 + paths: + - '.github/workflows/lint-rust.yml' + - 'plugins/*/src/**' + - '!plugins/*/src/api-iife.js' + - '**/Cargo.toml' + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +permissions: + contents: read + +jobs: + changes: + runs-on: ubuntu-latest + permissions: + pull-requests: read + outputs: + packages: ${{ steps.filter.outputs.changes }} + saver: ${{ steps.saver.outputs.saver }} + steps: + - uses: actions/checkout@v7 + - uses: dorny/paths-filter@v2 + id: filter + with: + filters: | + tauri-plugin-autostart: + - .github/workflows/lint-rust.yml + - plugins/autostart/** + tauri-plugin-cli: + - .github/workflows/lint-rust.yml + - plugins/cli/** + tauri-plugin-clipboard-manager: + - .github/workflows/lint-rust.yml + - plugins/clipboard-manager/** + tauri-plugin-deep-link: + - .github/workflows/lint-rust.yml + - plugins/deep-link/** + tauri-plugin-dialog: + - .github/workflows/lint-rust.yml + - plugins/dialog/** + - plugins/fs/** + tauri-plugin-fs: + - .github/workflows/lint-rust.yml + - plugins/fs/** + tauri-plugin-geolocation: + - .github/workflows/lint-rust.yml + - plugins/geolocation/** + tauri-plugin-global-shortcut: + - .github/workflows/lint-rust.yml + - plugins/global-shortcut/** + tauri-plugin-opener: + - .github/workflows/lint-rust.yml + - plugins/opener/** + tauri-plugin-haptics: + - .github/workflows/lint-rust.yml + - plugins/haptics/** + tauri-plugin-http: + - .github/workflows/lint-rust.yml + - plugins/http/** + - plugins/fs/** + tauri-plugin-localhost: + - .github/workflows/lint-rust.yml + - plugins/localhost/** + tauri-plugin-log: + - .github/workflows/lint-rust.yml + - plugins/log/** + tauri-plugin-notification: + - .github/workflows/lint-rust.yml + - plugins/notification/** + tauri-plugin-os: + - .github/workflows/lint-rust.yml + - plugins/os/** + tauri-plugin-persisted-scope: + - .github/workflows/lint-rust.yml + - plugins/persisted-scope/** + - plugins/fs/** + tauri-plugin-positioner: + - .github/workflows/lint-rust.yml + - plugins/positioner/** + tauri-plugin-process: + - .github/workflows/lint-rust.yml + - plugins/process/** + tauri-plugin-shell: + - .github/workflows/lint-rust.yml + - plugins/shell/** + tauri-plugin-single-instance: + - .github/workflows/lint-rust.yml + - plugins/single-instance/** + tauri-plugin-sql: + - .github/workflows/lint-rust.yml + - plugins/sql/** + tauri-plugin-store: + - .github/workflows/lint-rust.yml + - plugins/store/** + tauri-plugin-stronghold: + - .github/workflows/lint-rust.yml + - plugins/stronghold/** + tauri-plugin-updater: + - .github/workflows/lint-rust.yml + - plugins/updater/** + tauri-plugin-upload: + - .github/workflows/lint-rust.yml + - plugins/upload/** + tauri-plugin-websocket: + - .github/workflows/lint-rust.yml + - plugins/websocket/** + tauri-plugin-window-state: + - .github/workflows/lint-rust.yml + - plugins/window-state/** + + # Every package job for a platform restores the same rust-cache entry, and exactly one + # of them saves it (see the `save-if` of the rust-cache step). Prefer the packages whose + # dependency tree covers most of the other plugins' dependencies, so the entry is as + # useful as possible for the packages that did not save it. + - name: pick the package that saves the cache + id: saver + env: + PACKAGES: ${{ steps.filter.outputs.changes }} + run: | + saver="$(jq -rn --argjson packages "${PACKAGES:-[]}" ' + ["tauri-plugin-upload", "tauri-plugin-sql", "tauri-plugin-updater", "tauri-plugin-single-instance"] + | map(select(. as $p | $packages | index($p) != null)) + | first // $packages[0] // empty + ')" + echo "saver=$saver" >> "$GITHUB_OUTPUT" + + clippy: + needs: changes + if: ${{ needs.changes.outputs.packages != '[]' && needs.changes.outputs.packages != '' }} + runs-on: ubuntu-22.04 + strategy: + fail-fast: false + matrix: + package: ${{ fromJSON(needs.changes.outputs.packages) }} + + steps: + - uses: actions/checkout@v7 + + - name: install webkit2gtk + run: | + sudo apt-get update + sudo apt-get install -y libwebkit2gtk-4.0-dev libwebkit2gtk-4.1-dev + + - name: Install clippy with stable toolchain + uses: dtolnay/rust-toolchain@stable + with: + components: clippy + + - uses: Swatinem/rust-cache@v2 + with: + # One entry shared by every package job (the default key is per job, not per + # matrix entry), only written from `v2`/`v3` and by a single package job; see + # test-rust.yml for the reasoning. + save-if: ${{ (github.ref == 'refs/heads/v2' || github.ref == 'refs/heads/v3') && matrix.package == needs.changes.outputs.saver }} + + - name: clippy ${{ matrix.package }} + run: cargo clippy --package ${{ matrix.package }} --all-targets -- -D warnings + + - name: clippy ${{ matrix.package }} --all-features + if: matrix.package != 'tauri-plugin-dialog' + run: cargo clippy --package ${{ matrix.package }} --all-targets --all-features -- -D warnings diff --git a/.github/workflows/sync.yml b/.github/workflows/sync.yml new file mode 100644 index 0000000..002f5bd --- /dev/null +++ b/.github/workflows/sync.yml @@ -0,0 +1,46 @@ +# Copyright 2019-2023 Tauri Programme within The Commons Conservancy +# SPDX-License-Identifier: Apache-2.0 +# SPDX-License-Identifier: MIT + +name: Sync + +on: + workflow_dispatch: + push: + branches: + - v1 + - v2 + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +permissions: + contents: read + +jobs: + sync-to-mirrors: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + + - name: Fetch git tags + run: git fetch origin 'refs/tags/*:refs/tags/*' + + - uses: actions/setup-node@v7 + with: + node-version: 'lts/*' + + - uses: pnpm/action-setup@v6 + with: + run_install: true + cache: true # the pnpm store, keyed by OS and lockfile hash and shared by every workflow + + - name: Build packages + run: pnpm build + + - name: Sync + run: .github/sync-to-mirrors.sh + env: + BUILD_BASE: ${{ github.workspace }}/plugins + API_TOKEN_GITHUB: ${{ secrets.ORG_TAURI_BOT_PAT }} diff --git a/.github/workflows/test-android.yml b/.github/workflows/test-android.yml new file mode 100644 index 0000000..45fdc08 --- /dev/null +++ b/.github/workflows/test-android.yml @@ -0,0 +1,78 @@ +# Copyright 2019-2023 Tauri Programme within The Commons Conservancy +# SPDX-License-Identifier: Apache-2.0 +# SPDX-License-Identifier: MIT + +name: test android + +on: + pull_request: + paths: + - '.github/workflows/test-android.yml' + - '**/android/**' + workflow_dispatch: + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +permissions: + contents: read + +jobs: + test: + runs-on: ${{ matrix.platform }} + + strategy: + fail-fast: false + matrix: + platform: [ubuntu-latest, macos-latest, windows-latest] + + # No caches on purpose: this workflow only runs on pull requests, so anything it saved + # would live on the merge ref where no other run can restore it, while still counting + # against the repository's 10 GB cache limit and evicting entries that `v2`/`v3` saved. + steps: + - uses: actions/checkout@v7 + + - uses: dtolnay/rust-toolchain@1.77.2 + + - uses: actions/setup-node@v7 + with: + node-version: 'lts/*' + + - uses: pnpm/action-setup@v6 + with: + run_install: true + + - uses: actions/setup-java@v5 + with: + distribution: temurin + java-version: 21 + + - name: Setup NDK + uses: nttld/setup-ndk@v1 + id: setup-ndk + with: + ndk-version: r25 + + # TODO check after https://github.com/nttld/setup-ndk/issues/518 is fixed + - name: Restore Android Symlinks + if: matrix.platform == 'ubuntu-latest' || matrix.platform == 'macos-latest' + env: + NDK_HOST: ${{ matrix.platform == 'ubuntu-latest' && 'linux-x86_64' || 'darwin-x86_64' }} + run: | + directory="${{ steps.setup-ndk.outputs.ndk-path }}/toolchains/llvm/prebuilt/$NDK_HOST/bin" + find "$directory" -type l | while read link; do + current_target=$(readlink "$link") + new_target="$directory/$(basename "$current_target")" + ln -sf "$new_target" "$link" + echo "Changed $(basename "$link") from $current_target to $new_target" + done + + - name: build API + run: pnpm build + + - name: build APK + working-directory: ./examples/api + run: pnpm tauri android build + env: + NDK_HOME: ${{ steps.setup-ndk.outputs.ndk-path }} diff --git a/.github/workflows/test-api-e2e-mobile.yml b/.github/workflows/test-api-e2e-mobile.yml new file mode 100644 index 0000000..c73cb89 --- /dev/null +++ b/.github/workflows/test-api-e2e-mobile.yml @@ -0,0 +1,235 @@ +# Copyright 2019-2023 Tauri Programme within The Commons Conservancy +# SPDX-License-Identifier: Apache-2.0 +# SPDX-License-Identifier: MIT + +name: test plugins e2e (mobile) + +on: + workflow_dispatch: + push: + branches: + - v2 + paths: + - '.github/workflows/test-api-e2e-mobile.yml' + - 'packages/api-e2e/**' + - 'examples/api/**' + - 'plugins/*/guest-js/**' + - 'plugins/*/src/**' + - 'plugins/*/android/**' + - 'plugins/*/ios/**' + - 'plugins/*/permissions/**' + - 'plugins/*/build.rs' + pull_request: + branches: + - v2 + paths: + - '.github/workflows/test-api-e2e-mobile.yml' + - 'packages/api-e2e/**' + - 'examples/api/**' + - 'plugins/*/guest-js/**' + - 'plugins/*/src/**' + - 'plugins/*/android/**' + - 'plugins/*/ios/**' + - 'plugins/*/permissions/**' + - 'plugins/*/build.rs' + +env: + RUST_BACKTRACE: 1 + CARGO_PROFILE_DEV_DEBUG: 0 # keeps the target folder small for better cache efficiency + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +permissions: + contents: read + +jobs: + android: + runs-on: ubuntu-latest + env: + # The emulator runs the host architecture, so only that target is built. + E2E_ANDROID_TARGET: x86_64 + + steps: + - uses: actions/checkout@v7 + + - name: install Rust stable + uses: dtolnay/rust-toolchain@stable + with: + targets: x86_64-linux-android + + - name: setup node + uses: actions/setup-node@v7 + with: + node-version: 'lts/*' + - uses: pnpm/action-setup@v6 + with: + cache: true # the pnpm store, keyed by OS and lockfile hash and shared by every workflow + + - uses: actions/setup-java@v5 + with: + distribution: temurin + java-version: 21 + + # Same rule as the rust-cache step below: every run restores the Gradle cache and only + # `v2`/`v3` save it. setup-java's own `cache: gradle` has no such switch, so the restore + # and save steps are spelled out here (same paths and key inputs as setup-java uses). + - name: restore gradle cache + id: gradle-cache + uses: actions/cache/restore@v6 + with: + path: | + ~/.gradle/caches + ~/.gradle/wrapper + key: gradle-${{ runner.os }}-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} + restore-keys: gradle-${{ runner.os }}- + + - name: Setup NDK + uses: nttld/setup-ndk@v1 + id: setup-ndk + with: + ndk-version: r25 + local-cache: true + + # TODO check after https://github.com/nttld/setup-ndk/issues/518 is fixed + - name: Restore Android Symlinks + run: | + directory="${{ steps.setup-ndk.outputs.ndk-path }}/toolchains/llvm/prebuilt/linux-x86_64/bin" + find "$directory" -type l | while read link; do + current_target=$(readlink "$link") + new_target="$directory/$(basename "$current_target")" + ln -sf "$new_target" "$link" + echo "Changed $(basename "$link") from $current_target to $new_target" + done + + # Hardware acceleration for the emulator. + - name: enable KVM + run: | + echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules + sudo udevadm control --reload-rules + sudo udevadm trigger --name-match=kvm + + - uses: Swatinem/rust-cache@v2 + with: + key: android + # Only `v2`/`v3` write to the cache: an entry saved from a pull request lives on its + # merge ref, where nothing else can restore it, and every write evicts an entry + # that other runs would have hit (10 GB repository limit, LRU eviction). + save-if: ${{ github.ref == 'refs/heads/v2' || github.ref == 'refs/heads/v3' }} + + - name: install dependencies + run: pnpm i --frozen-lockfile + + - name: build plugins + run: pnpm build + + # Built before the emulator is up so it does not sit idle (and time out) + # during the Rust and Gradle builds; the suite then reuses the APK. + - name: build the APK + working-directory: ./examples/api + env: + NDK_HOME: ${{ steps.setup-ndk.outputs.ndk-path }} + # a debug build without debug info, which would otherwise grow the APK + # past what the emulator can install (see androidBuildEnv in wdio.mobile.ts) + CARGO_PROFILE_DEV_DEBUG: '0' + run: pnpm tauri android build --debug --apk --target $E2E_ANDROID_TARGET + + - name: save gradle cache + if: ${{ (github.ref == 'refs/heads/v2' || github.ref == 'refs/heads/v3') && steps.gradle-cache.outputs.cache-hit != 'true' }} + uses: actions/cache/save@v6 + with: + path: | + ~/.gradle/caches + ~/.gradle/wrapper + key: ${{ steps.gradle-cache.outputs.cache-primary-key }} + + - name: run e2e tests + uses: reactivecircus/android-emulator-runner@v2 + timeout-minutes: 60 + with: + api-level: 35 + arch: x86_64 + # google_apis images ship an up-to-date Android System WebView. + target: google_apis + profile: pixel_6 + emulator-options: -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none + disable-animations: true + script: E2E_SKIP_BUILD=1 E2E_SPEC_RETRIES=1 pnpm test:api-e2e:android + + - name: upload appium logs + if: failure() + uses: actions/upload-artifact@v7 + with: + name: android-appium-logs + path: packages/api-e2e/logs + if-no-files-found: ignore + + ios: + runs-on: macos-latest + + steps: + - uses: actions/checkout@v7 + + - name: install Rust stable + uses: dtolnay/rust-toolchain@stable + with: + targets: aarch64-apple-ios-sim + + - name: setup node + uses: actions/setup-node@v7 + with: + node-version: 'lts/*' + - uses: pnpm/action-setup@v6 + with: + cache: true # the pnpm store, keyed by OS and lockfile hash and shared by every workflow + + - uses: Swatinem/rust-cache@v2 + with: + key: ios + # Only `v2`/`v3` write to the cache: an entry saved from a pull request lives on its + # merge ref, where nothing else can restore it, and every write evicts an entry + # that other runs would have hit (10 GB repository limit, LRU eviction). + save-if: ${{ github.ref == 'refs/heads/v2' || github.ref == 'refs/heads/v3' }} + + - name: install dependencies + run: pnpm i --frozen-lockfile + + - name: build plugins + run: pnpm build + + # Simulator build (`aarch64-sim` on the Apple Silicon runners), unsigned. + # Built ahead of the suite for symmetry with the Android job. + - name: build the simulator app + working-directory: ./examples/api + run: pnpm tauri ios build --debug --target aarch64-sim --no-sign + + # wdio.ios.conf picks the iPhone simulator on the newest installed runtime + # (no device name is hardcoded) and Appium boots it; the XCUITest driver + # compiles WebDriverAgent on the first session. + - name: run e2e tests + timeout-minutes: 60 + env: + E2E_SKIP_BUILD: '1' + E2E_SPEC_RETRIES: '1' + run: pnpm test:api-e2e:ios + + - name: upload appium logs + if: failure() + uses: actions/upload-artifact@v7 + with: + name: ios-appium-logs + path: packages/api-e2e/logs + if-no-files-found: ignore + + - name: collect app crash reports + if: failure() + run: .scripts/ci/collect-macos-crash-reports.sh "$RUNNER_TEMP/crash-reports" + + - name: upload app crash reports + if: failure() + uses: actions/upload-artifact@v7 + with: + name: ios-crash-reports + path: ${{ runner.temp }}/crash-reports + if-no-files-found: ignore diff --git a/.github/workflows/test-api-e2e.yml b/.github/workflows/test-api-e2e.yml new file mode 100644 index 0000000..3c18ad5 --- /dev/null +++ b/.github/workflows/test-api-e2e.yml @@ -0,0 +1,176 @@ +# Copyright 2019-2023 Tauri Programme within The Commons Conservancy +# SPDX-License-Identifier: Apache-2.0 +# SPDX-License-Identifier: MIT + +name: test plugins e2e + +on: + workflow_dispatch: + push: + branches: + - v2 + paths: + - '.github/workflows/test-api-e2e.yml' + - 'packages/api-e2e/**' + - 'examples/api/**' + - 'plugins/*/guest-js/**' + - 'plugins/*/src/**' + - 'plugins/*/permissions/**' + - 'plugins/*/build.rs' + pull_request: + branches: + - v2 + paths: + - '.github/workflows/test-api-e2e.yml' + - 'packages/api-e2e/**' + - 'examples/api/**' + - 'plugins/*/guest-js/**' + - 'plugins/*/src/**' + - 'plugins/*/permissions/**' + - 'plugins/*/build.rs' + +env: + RUST_BACKTRACE: 1 + CARGO_PROFILE_DEV_DEBUG: 0 # keeps the target folder small for better cache efficiency + MSEDGEDRIVER_TOOL_REV: 8c4b34f51b45f5cf08013366d703de464ab871d1 + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +permissions: + contents: read + +jobs: + test: + runs-on: ${{ matrix.platform.os }} + + strategy: + fail-fast: false + matrix: + platform: + - { name: Linux, os: ubuntu-latest } + - { name: Windows, os: windows-latest } + - { name: macOS, os: macos-latest } + + steps: + - uses: actions/checkout@v7 + + - name: install Rust stable + uses: dtolnay/rust-toolchain@stable + + - name: setup node + uses: actions/setup-node@v7 + with: + node-version: 'lts/*' + - uses: pnpm/action-setup@v6 + with: + cache: true # the pnpm store, keyed by OS and lockfile hash and shared by every workflow + + - name: install Linux dependencies + if: runner.os == 'Linux' + run: | + sudo apt-get update + sudo apt-get install -y \ + libwebkit2gtk-4.1-dev \ + webkit2gtk-driver \ + libgtk-3-dev \ + libayatana-appindicator3-dev \ + librsvg2-dev \ + dbus \ + xvfb \ + fluxbox \ + desktop-file-utils \ + xdg-utils + + # msedgedriver has to match the installed WebView2 runtime, so msedgedriver-tool + # downloads the right version at run time. The tool itself is built from a pinned git + # revision and kept in its own small cache entry (accessed by every Windows run, so it + # stays hot) instead of being rebuilt from source on every run. + - name: restore msedgedriver-tool (Windows) + if: runner.os == 'Windows' + id: msedgedriver-tool-cache + uses: actions/cache@v6 + with: + path: ${{ runner.tool_cache }}/msedgedriver-tool + key: msedgedriver-tool-${{ runner.os }}-${{ env.MSEDGEDRIVER_TOOL_REV }} + + - name: install msedgedriver-tool (Windows) + if: runner.os == 'Windows' && steps.msedgedriver-tool-cache.outputs.cache-hit != 'true' + run: cargo install --git https://github.com/chippers/msedgedriver-tool --rev ${{ env.MSEDGEDRIVER_TOOL_REV }} --locked --root ${{ runner.tool_cache }}/msedgedriver-tool + + - name: install msedgedriver (Windows) + if: runner.os == 'Windows' + run: | + & "${{ runner.tool_cache }}/msedgedriver-tool/bin/msedgedriver-tool.exe" + $PWD.Path >> $env:GITHUB_PATH + + - uses: Swatinem/rust-cache@v2 + with: + key: ${{ matrix.platform.os }} + # Only `v2`/`v3` write to the cache: an entry saved from a pull request lives on its + # merge ref, where nothing else can restore it, and every write evicts an entry + # that other runs would have hit (10 GB repository limit, LRU eviction). + save-if: ${{ github.ref == 'refs/heads/v2' || github.ref == 'refs/heads/v3' }} + + - name: install dependencies + run: pnpm i --frozen-lockfile + + - name: build plugins + run: pnpm build + + - name: run e2e tests (Linux) + if: runner.os == 'Linux' + timeout-minutes: 45 + # A real window manager (fluxbox) is started so that window-state tests + # (size restore) behave, and a session D-Bus so the notification and + # clipboard plugins can reach their desktop services. + env: + E2E_SPEC_RETRIES: '1' + run: | + xvfb-run --auto-servernum -- dbus-run-session -- bash -c ' + fluxbox >/dev/null 2>&1 & + sleep 1 + pnpm test:api-e2e + ' + + - name: run e2e tests (Windows) + if: runner.os == 'Windows' + timeout-minutes: 45 + shell: pwsh + # msedgedriver hands the app the `--remote-debugging-port` it attaches to through + # `WEBVIEW2_ADDITIONAL_BROWSER_ARGUMENTS`, and WebView2 ignores that variable in an + # elevated process, which is what the runner gives us. gsudo runs the suite at medium + # integrity instead, and the workspace is opened up so the de-elevated process can use it. + # See https://github.com/tauri-apps/wry/issues/1782 and + # https://github.com/MicrosoftEdge/WebView2Feedback/issues/5645. + env: + E2E_SPEC_RETRIES: '1' + run: | + winget install gerardog.gsudo --accept-source-agreements --accept-package-agreements + icacls "$env:GITHUB_WORKSPACE" /grant "Everyone:(OI)(CI)F" + sudo --integrity Medium pnpm test:api-e2e + + - name: run e2e tests (macOS) + if: runner.os == 'macOS' && env.CN_API_KEY != '' + timeout-minutes: 45 + env: + CN_API_KEY: ${{ secrets.TAURI_E2E_CN_API_KEY }} + E2E_SPEC_RETRIES: '1' + # macOS has no native WebDriver for WKWebView, so the app is driven through the + # CrabNebula Webdriver (tauri-plugin-automation + test-runner-backend), which + # authenticates with CN_API_KEY. wdio.conf enables it automatically on darwin and + # builds the `.app` bundle it needs. + run: pnpm test:api-e2e + + - name: collect app crash reports (macOS) + if: runner.os == 'macOS' && failure() + run: .scripts/ci/collect-macos-crash-reports.sh "$RUNNER_TEMP/crash-reports" + + - name: upload app crash reports (macOS) + if: runner.os == 'macOS' && failure() + uses: actions/upload-artifact@v7 + with: + name: macos-crash-reports + path: ${{ runner.temp }}/crash-reports + if-no-files-found: ignore diff --git a/.github/workflows/test-rust.yml b/.github/workflows/test-rust.yml new file mode 100644 index 0000000..7fda7d3 --- /dev/null +++ b/.github/workflows/test-rust.yml @@ -0,0 +1,311 @@ +# Copyright 2019-2023 Tauri Programme within The Commons Conservancy +# SPDX-License-Identifier: Apache-2.0 +# SPDX-License-Identifier: MIT + +name: Test Rust + +on: + push: + branches: + - v1 + - v2 + paths: + - '.github/workflows/test-rust.yml' + - 'plugins/*/src/**' + - '!plugins/*/src/api-iife.js' + - '**/Cargo.toml' + - '**/Cargo.lock' + pull_request: + branches: + - v1 + - v2 + paths: + - '.github/workflows/test-rust.yml' + - 'plugins/*/src/**' + - '!plugins/*/src/api-iife.js' + - '**/Cargo.toml' + - '**/Cargo.lock' + +env: + CARGO_PROFILE_DEV_DEBUG: 0 # no debuginfo: faster builds and a much smaller target dir to cache + CROSS_REV: 51f46f296253d8122c927c5bb933e3c4f27cc317 + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +permissions: + contents: read + +jobs: + changes: + runs-on: ubuntu-latest + permissions: + pull-requests: read + outputs: + packages: ${{ steps.filter.outputs.changes }} + saver: ${{ steps.saver.outputs.saver }} + steps: + - uses: actions/checkout@v7 + - uses: dorny/paths-filter@v2 + id: filter + with: + base: v2 + filters: | + tauri-plugin-autostart: + - .github/workflows/test-rust.yml + - Cargo.toml + - Cargo.lock + - plugins/autostart/** + tauri-plugin-cli: + - .github/workflows/test-rust.yml + - Cargo.toml + - Cargo.lock + - plugins/cli/** + tauri-plugin-clipboard-manager: + - .github/workflows/test-rust.yml + - Cargo.toml + - Cargo.lock + - plugins/clipboard-manager/** + tauri-plugin-deep-link: + - .github/workflows/test-rust.yml + - Cargo.toml + - Cargo.lock + - plugins/deep-link/** + tauri-plugin-dialog: + - .github/workflows/test-rust.yml + - Cargo.toml + - Cargo.lock + - plugins/dialog/** + - plugins/fs/** + tauri-plugin-fs: + - .github/workflows/test-rust.yml + - Cargo.toml + - Cargo.lock + - plugins/fs/** + tauri-plugin-geolocation: + - .github/workflows/test-rust.yml + - Cargo.toml + - Cargo.lock + - plugins/geolocation/** + tauri-plugin-global-shortcut: + - .github/workflows/test-rust.yml + - Cargo.toml + - Cargo.lock + - plugins/global-shortcut/** + tauri-plugin-opener: + - .github/workflows/test-rust.yml + - Cargo.toml + - Cargo.lock + - plugins/opener/** + tauri-plugin-haptics: + - .github/workflows/test-rust.yml + - Cargo.toml + - Cargo.lock + - plugins/haptics/** + tauri-plugin-http: + - .github/workflows/test-rust.yml + - Cargo.toml + - Cargo.lock + - plugins/http/** + - plugins/fs/** + tauri-plugin-localhost: + - .github/workflows/test-rust.yml + - Cargo.toml + - Cargo.lock + - plugins/localhost/** + tauri-plugin-log: + - .github/workflows/test-rust.yml + - Cargo.toml + - Cargo.lock + - plugins/log/** + tauri-plugin-notification: + - .github/workflows/test-rust.yml + - Cargo.toml + - Cargo.lock + - plugins/notification/** + tauri-plugin-os: + - .github/workflows/test-rust.yml + - Cargo.toml + - Cargo.lock + - plugins/os/** + tauri-plugin-persisted-scope: + - .github/workflows/test-rust.yml + - Cargo.toml + - Cargo.lock + - plugins/persisted-scope/** + - plugins/fs/** + tauri-plugin-positioner: + - .github/workflows/test-rust.yml + - Cargo.toml + - Cargo.lock + - plugins/positioner/** + tauri-plugin-process: + - .github/workflows/test-rust.yml + - Cargo.toml + - Cargo.lock + - plugins/process/** + tauri-plugin-shell: + - .github/workflows/test-rust.yml + - Cargo.toml + - Cargo.lock + - plugins/shell/** + tauri-plugin-single-instance: + - .github/workflows/test-rust.yml + - Cargo.toml + - Cargo.lock + - plugins/single-instance/** + tauri-plugin-sql: + - .github/workflows/test-rust.yml + - Cargo.toml + - Cargo.lock + - plugins/sql/** + tauri-plugin-store: + - .github/workflows/test-rust.yml + - Cargo.toml + - Cargo.lock + - plugins/store/** + tauri-plugin-stronghold: + - .github/workflows/test-rust.yml + - Cargo.toml + - Cargo.lock + - plugins/stronghold/** + tauri-plugin-updater: + - .github/workflows/test-rust.yml + - Cargo.toml + - Cargo.lock + - plugins/updater/** + tauri-plugin-upload: + - .github/workflows/test-rust.yml + - Cargo.toml + - Cargo.lock + - plugins/upload/** + tauri-plugin-websocket: + - .github/workflows/test-rust.yml + - Cargo.toml + - Cargo.lock + - plugins/websocket/** + tauri-plugin-window-state: + - .github/workflows/test-rust.yml + - Cargo.toml + - Cargo.lock + - plugins/window-state/** + + # Every package job for a platform restores the same rust-cache entry, and exactly one + # of them saves it (see the `save-if` of the rust-cache step). Prefer the packages whose + # dependency tree covers most of the other plugins' dependencies, so the entry is as + # useful as possible for the packages that did not save it. + - name: pick the package that saves the cache + id: saver + env: + PACKAGES: ${{ steps.filter.outputs.changes }} + run: | + saver="$(jq -rn --argjson packages "${PACKAGES:-[]}" ' + ["tauri-plugin-upload", "tauri-plugin-sql", "tauri-plugin-updater", "tauri-plugin-single-instance"] + | map(select(. as $p | $packages | index($p) != null)) + | first // $packages[0] // empty + ')" + echo "saver=$saver" >> "$GITHUB_OUTPUT" + + test: + needs: changes + if: ${{ needs.changes.outputs.packages != '[]' && needs.changes.outputs.packages != '' }} + strategy: + fail-fast: false + matrix: + package: ${{ fromJSON(needs.changes.outputs.packages) }} + platform: + - { + target: x86_64-pc-windows-msvc, + os: windows-latest, + runner: 'cargo', + command: 'test' + } + - { + target: x86_64-unknown-linux-gnu, + os: ubuntu-22.04, + runner: 'cargo', + command: 'test' + } + - { + target: aarch64-apple-darwin, + os: macos-latest, + runner: 'cargo', + command: 'test' + } + - { + target: aarch64-apple-ios, + os: macos-latest, + runner: 'cargo', + command: 'build' + } + - { + target: aarch64-linux-android, + os: ubuntu-latest, + runner: 'cross', + command: 'build' + } + + runs-on: ${{ matrix.platform.os }} + + steps: + - uses: actions/checkout@v7 + + - name: install webkit2gtk + if: contains(matrix.platform.target, 'unknown-linux') + run: | + sudo apt-get update + sudo apt-get install -y libwebkit2gtk-4.0-dev libwebkit2gtk-4.1-dev + + - uses: dtolnay/rust-toolchain@1.77.2 + with: + targets: ${{ matrix.platform.target }} + + - uses: Swatinem/rust-cache@v2 + with: + # One entry per platform, shared by every package job: the plugins share most of + # their dependency tree (tauri, wry, tao...), and an entry per package and platform + # (26 × 5 entries of 500-800 MB each) never fits the repository's 10 GB cache limit. + key: ${{ matrix.platform.target }} + # Only `v2`/`v3` write to the cache: an entry saved from a pull request lives on its + # merge ref, where nothing else can restore it, and every write evicts an entry + # that other runs would have hit (10 GB repository limit, LRU eviction). + # A single package job saves the shared entry, so the jobs don't race for the key. + save-if: ${{ (github.ref == 'refs/heads/v2' || github.ref == 'refs/heads/v3') && matrix.package == needs.changes.outputs.saver }} + + # `cross` is built from a pinned git revision (its latest release predates the Android + # targets we need). Keep the binary in its own small cache entry (accessed by every run, + # so it stays hot) instead of rebuilding it from source on every run. + - name: restore cross + if: ${{ matrix.platform.runner == 'cross' }} + id: cross-cache + uses: actions/cache@v6 + with: + path: ${{ runner.tool_cache }}/cross + key: cross-${{ runner.os }}-${{ env.CROSS_REV }} + + - name: install cross + if: ${{ matrix.platform.runner == 'cross' && steps.cross-cache.outputs.cache-hit != 'true' }} + run: cargo +stable install cross --git https://github.com/cross-rs/cross --rev ${{ env.CROSS_REV }} --locked --root ${{ runner.tool_cache }}/cross + + - name: add cross to PATH + if: ${{ matrix.platform.runner == 'cross' }} + run: echo "${{ runner.tool_cache }}/cross/bin" >> $GITHUB_PATH + + - name: test ${{ matrix.package }} + if: ${{ matrix.package != 'tauri-plugin-http' && matrix.package != 'tauri-plugin-dialog' }} + run: ${{ matrix.platform.runner }} ${{ matrix.platform.command }} --package ${{ matrix.package }} --target ${{ matrix.platform.target }} --all-targets --all-features + + - name: test ${{ matrix.package }} + if: ${{ matrix.package == 'tauri-plugin-http' || matrix.package == 'tauri-plugin-dialog' }} + run: ${{ matrix.platform.runner }} ${{ matrix.platform.command }} --package ${{ matrix.package }} --target ${{ matrix.platform.target }} --all-targets + + # `--all-targets` above does not include doc tests, so they are run separately. + # Only on the platforms we actually test on, since cargo skips doc tests when cross compiling. + - name: doc test ${{ matrix.package }} + if: ${{ matrix.platform.command == 'test' && matrix.package != 'tauri-plugin-http' && matrix.package != 'tauri-plugin-dialog' }} + run: ${{ matrix.platform.runner }} test --doc --package ${{ matrix.package }} --target ${{ matrix.platform.target }} --all-features + + - name: doc test ${{ matrix.package }} + if: ${{ matrix.platform.command == 'test' && (matrix.package == 'tauri-plugin-http' || matrix.package == 'tauri-plugin-dialog') }} + run: ${{ matrix.platform.runner }} test --doc --package ${{ matrix.package }} --target ${{ matrix.platform.target }} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f5110d5 --- /dev/null +++ b/.gitignore @@ -0,0 +1,62 @@ +# dependency directories +node_modules/ +target/ + +# Optional npm and yarn cache directory +.npm/ +.yarn/ + +# Output of 'npm pack' +*.tgz + +# dotenv environment variables file +.env + +# .vscode workspace settings file +.vscode/settings.json +.vscode/launch.json +.vscode/tasks.json + +# npm, yarn and bun lock files +package-lock.json +yarn.lock +bun.lockb +bun.lock + +# rust compiled folders +target/ + +# compiled plugins +dist-js/ + +# plugins .tauri directory +/plugins/*/.tauri + +# examples +examples/*/dist +plugins/*/examples/*/dist +examples/*/src-tauri/gen/schemas +plugins/*/examples/*/src-tauri/gen/schemas + +# logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# runtime data +pids +*.pid +*.seed +*.pid.lock + +# miscellaneous +/.vs +.DS_Store +.Thumbs.db +*.sublime* +.idea +debug.log +TODO.md +.aider.* diff --git a/.npmrc b/.npmrc new file mode 100644 index 0000000..30ab299 --- /dev/null +++ b/.npmrc @@ -0,0 +1 @@ +link-workspace-packages=true diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..bc4fca6 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,27 @@ +/.changes +/.vscode + +# dependcies and artifacts directories +node_modules/ +target/ +dist-js/ +dist/ + +# lock files +pnpm-lock.yaml + +# examples gen directory +examples/*/src-tauri/gen/ +plugins/*/examples/*/src-tauri/gen/ + +# autogenerated files +**/autogenerated/**/*.md +api-iife.js +init-iife.js +CHANGELOG.md +*schema.json + +# mobile build +**/ios/.build +**/.tauri +plugins/*/android/build diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..6ca6fdd --- /dev/null +++ b/.prettierrc @@ -0,0 +1,6 @@ +{ + "singleQuote": true, + "semi": false, + "trailingComma": "none", + "experimentalOperatorPosition": "start" +} diff --git a/.scripts/ci/check-change-files.js b/.scripts/ci/check-change-files.js new file mode 100644 index 0000000..c9ff7e9 --- /dev/null +++ b/.scripts/ci/check-change-files.js @@ -0,0 +1,86 @@ +#!/usr/bin/env node + +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +import { readFileSync, readdirSync } from 'fs' +import { join } from 'path' + +/* const ignorePackages = [ + 'api-example', + 'api-example-js', + 'deep-link-example', + 'deep-link-example-js' +] */ + +const rsOnly = ['localhost', 'persisted-scope', 'single-instance'] + +function checkChangeFiles(changeFiles) { + let code = 0 + + for (const file of changeFiles) { + const content = readFileSync(file, 'utf8') + const [frontMatter] = /^---[\s\S.]*---\n/i.exec(content) + const packages = frontMatter + .split('\n') + .filter((l) => !(l === '---' || !l)) + .map((l) => l.replace(/('|")/g, '').split(':')) + + const rsPackages = Object.fromEntries( + packages + .filter((v) => !v[0].endsWith('-js')) + .map((v) => [v[0], v[1].trim()]) + ) + const jsPackages = Object.fromEntries( + packages + .filter((v) => v[0].endsWith('-js')) + .map((v) => [v[0].slice(0, -3), v[1].trim()]) + ) + + for (const pkg in rsPackages) { + if (rsOnly.includes(pkg)) continue + + if (!jsPackages[pkg]) { + console.error( + `Missing "${rsPackages[pkg]}" bump for JS package "${pkg}-js" in ${file}.` + ) + code = 1 + } else if (rsPackages[pkg] != jsPackages[pkg]) { + console.error( + `"${pkg}" and "${pkg}-js" have different version bumps in ${file}.` + ) + code = 1 + } + } + + for (const pkg in jsPackages) { + if (!rsPackages[pkg]) { + console.error( + `Missing "${jsPackages[pkg]}" bump for Rust package "${pkg}" in ${file}.` + ) + code = 1 + } else if (rsPackages[pkg] != jsPackages[pkg]) { + console.error( + `"${pkg}" and "${pkg}-js" have different version bumps in ${file}.` + ) + code = 1 + } + } + } + + process.exit(code) +} + +const [_bin, _script, ...files] = process.argv + +if (files.length > 0) { + checkChangeFiles( + files.filter((f) => f.toLowerCase() !== '.changes/readme.md') + ) +} else { + const changeFiles = readdirSync('.changes') + .filter((f) => f.endsWith('.md') && f.toLowerCase() !== 'readme.md') + .map((p) => join('.changes', p)) + checkChangeFiles(changeFiles) +} diff --git a/.scripts/ci/check-license-header.js b/.scripts/ci/check-license-header.js new file mode 100644 index 0000000..4341e5a --- /dev/null +++ b/.scripts/ci/check-license-header.js @@ -0,0 +1,130 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +import fs from 'fs' +import path from 'path' +import readline from 'readline' + +const header = `Copyright 2019-2023 Tauri Programme within The Commons Conservancy +SPDX-License-Identifier: Apache-2.0 +SPDX-License-Identifier: MIT` +const ignoredLicenses = [ + '// Copyright 2021 Flavio Oliveira', + '// Copyright 2021 Jonas Kruckenberg', + '// Copyright 2018-2023 the Deno authors.' +] + +const extensions = ['.rs', '.js', '.ts', '.yml', '.swift', '.kt'] +const ignore = [ + 'target', + 'templates', + 'node_modules', + 'gen', + 'dist', + 'dist-js', + '.svelte-kit', + 'api-iife.js', + 'init-iife.js', + '.build', + 'notify_rust' +] + +async function checkFile(file) { + if ( + extensions.some((e) => file.endsWith(e)) + && !ignore.some((i) => file.includes(`${path.sep}${i}`)) + ) { + const fileStream = fs.createReadStream(file) + const rl = readline.createInterface({ + input: fileStream, + crlfDelay: Infinity + }) + + let contents = `` + let i = 0 + for await (let line of rl) { + // ignore empty lines, allow shebang, swift-tools-version and bundler license + if ( + line.length === 0 + || line.startsWith('#!') + || line.startsWith('// swift-tools-version:') + || ignoredLicenses.includes(line) + ) { + continue + } + + // strip comment marker + if (line.startsWith('// ')) { + line = line.substring(3) + } else if (line.startsWith('# ')) { + line = line.substring(2) + } + + contents += line + if (++i === 3) { + break + } + contents += '\n' + } + if (contents !== header) { + return true + } + } + return false +} + +async function check(src) { + const missingHeader = [] + + for (const entry of fs.readdirSync(src, { + withFileTypes: true + })) { + const p = path.join(src, entry.name) + + if (entry.isSymbolicLink() || ignore.includes(entry.name)) { + continue + } + + if (entry.isDirectory()) { + const missing = await check(p) + missingHeader.push(...missing) + } else { + const isMissing = await checkFile(p) + if (isMissing) { + missingHeader.push(p) + } + } + } + + return missingHeader +} + +const [_bin, _script, ...files] = process.argv + +if (files.length > 0) { + async function run() { + const missing = [] + for (const f of files) { + const isMissing = await checkFile(f) + if (isMissing) { + missing.push(f) + } + } + if (missing.length > 0) { + console.log(missing.join('\n')) + process.exit(1) + } + } + + run() +} else { + check(path.resolve(new URL(import.meta.url).pathname, '../../..')).then( + (missing) => { + if (missing.length > 0) { + console.log(missing.join('\n')) + process.exit(1) + } + } + ) +} diff --git a/.scripts/ci/collect-macos-crash-reports.sh b/.scripts/ci/collect-macos-crash-reports.sh new file mode 100755 index 0000000..1706707 --- /dev/null +++ b/.scripts/ci/collect-macos-crash-reports.sh @@ -0,0 +1,50 @@ +#!/bin/bash + +# Copyright 2019-2023 Tauri Programme within The Commons Conservancy +# SPDX-License-Identifier: Apache-2.0 +# SPDX-License-Identifier: MIT + +# Collects and prints macOS crash reports after a failed plugins e2e run. +# +# The CrabNebula Webdriver proxies every command to a server running *inside* the app +# process, so when the app dies the suite only ever reports `connection refused` — the +# actual cause is never in the job log. The app's stdout/stderr is inherited and no panic +# message is printed, which means it goes down on a signal. macOS records the reason in a +# crash report, and that is the only place it survives the run. +# +# Copies every report into $1 (default: $RUNNER_TEMP/crash-reports) for upload. + +set -euo pipefail + +dest="${1:-${RUNNER_TEMP:-/tmp}/crash-reports}" +mkdir -p "$dest" + +# ReportCrash writes asynchronously; give the last crash a moment to land. +sleep 5 + +found=0 +for f in "$HOME/Library/Logs/DiagnosticReports"/*.ips \ + "$HOME/Library/Logs/DiagnosticReports/Retired"/*.ips; do + [ -e "$f" ] || continue + found=$((found + 1)) + cp "$f" "$dest/" + echo "::group::$(basename "$f")" + # An .ips file is a one-line JSON header followed by the JSON report body. + head -n 1 "$f" + tail -n +2 "$f" | jq -r ' + "exception: \(.exception // {} | tojson)", + "termination: \(.termination // {} | tojson)", + "asi: \(.asi // {} | tojson)", + "faulting thread \(.faultingThread // 0):", + (. as $r + | $r.threads[$r.faultingThread // 0].frames[]? + | " \($r.usedImages[.imageIndex].name // "?") \(.symbol // "?") +\(.imageOffset)"), + "last ObjC exception:", + (. as $r + | $r.lastExceptionBacktrace[]? + | " \($r.usedImages[.imageIndex].name // "?") \(.symbol // "?") +\(.imageOffset)") + ' || cat "$f" + echo "::endgroup::" +done + +[ "$found" -gt 0 ] || echo "no crash reports under $HOME/Library/Logs/DiagnosticReports" diff --git a/.scripts/ci/has-diff.sh b/.scripts/ci/has-diff.sh new file mode 100755 index 0000000..1f18ac6 --- /dev/null +++ b/.scripts/ci/has-diff.sh @@ -0,0 +1,10 @@ +#!/bin/bash + +if git diff --quiet --ignore-submodules HEAD +then + echo "working directory is clean" +else + echo "found diff" + git diff --name-status HEAD + exit 1 +fi diff --git a/.taurignore b/.taurignore new file mode 100644 index 0000000..28a49db --- /dev/null +++ b/.taurignore @@ -0,0 +1,2 @@ +plugins/*/permissions/autogenerated/ +plugins/*/android/.tauri/tauri-api/build/ diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000..68acfc9 --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,8 @@ +{ + "recommendations": [ + "rust-lang.rust-analyzer", + "EditorConfig.EditorConfig", + "esbenp.prettier-vscode", + "tamasfe.even-better-toml" + ] +} diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..23e38cb --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,58 @@ +[workspace] +members = [ + "plugins/*", + "plugins/*/tests/*", + "plugins/updater/tests/updater-migration/v2-app", + "plugins/*/examples/*/src-tauri", + "examples/*/src-tauri", +] +resolver = "2" + +[workspace.dependencies] +serde = { version = "1", features = ["derive"] } +tracing = "0.1" +log = "0.4.21" +tauri = { version = "2.10", default-features = false } +tauri-build = "2.5" +tauri-plugin = "2.5" +tauri-utils = "2.8" +serde_json = "1" +thiserror = "2" +url = "2" +schemars = "0.8" +dunce = "1" +specta = "^2.0.0-rc.16" +glob = "0.3" +zbus = "5.9" + +[workspace.package] +edition = "2021" +authors = ["Tauri Programme within The Commons Conservancy"] +license = "Apache-2.0 OR MIT" +rust-version = "1.77.2" +repository = "https://github.com/tauri-apps/plugins-workspace" + +# Stronghold encrypts its snapshots with scrypt: unoptimized, saving or loading one +# takes ~45s instead of ~1s, which stalls the (debug) example app and its e2e suite. +[profile.dev.package.scrypt] +opt-level = 3 +[profile.dev.package.salsa20] +opt-level = 3 +[profile.dev.package.pbkdf2] +opt-level = 3 +[profile.dev.package.sha2] +opt-level = 3 +[profile.dev.package.hmac] +opt-level = 3 +[profile.dev.package.iota-crypto] +opt-level = 3 +[profile.dev.package.rust-argon2] +opt-level = 3 + +# default to small, optimized release binaries +[profile.release] +panic = "abort" +codegen-units = 1 +lto = true +incremental = false +opt-level = "s" diff --git a/LICENSE.spdx b/LICENSE.spdx new file mode 100644 index 0000000..cdd0df5 --- /dev/null +++ b/LICENSE.spdx @@ -0,0 +1,20 @@ +SPDXVersion: SPDX-2.1 +DataLicense: CC0-1.0 +PackageName: tauri +DataFormat: SPDXRef-1 +PackageSupplier: Organization: The Tauri Programme in the Commons Conservancy +PackageHomePage: https://tauri.app +PackageLicenseDeclared: Apache-2.0 +PackageLicenseDeclared: MIT +PackageCopyrightText: 2019-2022, The Tauri Programme in the Commons Conservancy +PackageSummary: Tauri is a rust project that enables developers to make secure +and small desktop applications using a web frontend. + +PackageComment: The package includes the following libraries; see +Relationship information. + +Created: 2019-05-20T09:00:00Z +PackageDownloadLocation: git://github.com/tauri-apps/tauri +PackageDownloadLocation: git+https://github.com/tauri-apps/tauri.git +PackageDownloadLocation: git+ssh://github.com/tauri-apps/tauri.git +Creator: Person: Daniel Thompson-Yvetot \ No newline at end of file diff --git a/LICENSE_APACHE-2.0 b/LICENSE_APACHE-2.0 new file mode 100644 index 0000000..4947287 --- /dev/null +++ b/LICENSE_APACHE-2.0 @@ -0,0 +1,177 @@ + + 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 \ No newline at end of file diff --git a/LICENSE_MIT b/LICENSE_MIT new file mode 100644 index 0000000..4d75472 --- /dev/null +++ b/LICENSE_MIT @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2017 - Present Tauri Apps 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. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..d6f96c2 --- /dev/null +++ b/README.md @@ -0,0 +1,62 @@ +# Official Tauri Plugins + +This repo and all plugins require a Rust version of at least **1.77.2** + +## Plugins Found Here + +| | | Win | Mac | Lin | iOS | And | +| ---------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- | --- | --- | --- | --- | --- | +| [autostart](plugins/autostart) | Automatically launch your app at system startup. | ✅ | ✅ | ✅ | ❌ | ❌ | +| [barcode-scanner](plugins/barcode-scanner) | Allows your mobile application to use the camera to scan QR codes, EAN-13 and other kinds of barcodes. | ? | ? | ? | ✅ | ✅ | +| [biometric](plugins/biometric) | Prompt the user for biometric authentication on Android and iOS. | ? | ? | ? | ✅ | ✅ | +| [cli](plugins/cli) | Parse arguments from your Command Line Interface | ✅ | ✅ | ✅ | ❌ | ❌ | +| [clipboard-manager](plugins/clipboard-manager) | Read and write to the system clipboard. | ✅ | ✅ | ✅ | ✅ | ✅ | +| [deep-link](plugins/deep-link) | Set your Tauri application as the default handler for an URL. | ✅ | ✅ | ✅ | ✅ | ✅ | +| [dialog](plugins/dialog) | Native system dialogs for opening and saving files along with message dialogs. | ✅ | ✅ | ✅ | ✅ | ✅ | +| [fs](plugins/fs) | Access the file system. | ✅ | ✅ | ✅ | ? | ? | +| [geolocation](plugins/geolocation) | Get and track current device position. | ? | ? | ? | ✅ | ✅ | +| [global-shortcut](plugins/global-shortcut) | Register global shortcuts. | ✅ | ✅ | ✅ | ? | ? | +| [haptics](plugins/haptics) | Haptic feedback and vibrations. | ? | ? | ? | ✅ | ✅ | +| [http](plugins/http) | Access the HTTP client written in Rust. | ✅ | ✅ | ✅ | ✅ | ✅ | +| [localhost](plugins/localhost) | Use a localhost server in production apps. | ✅ | ✅ | ✅ | ? | ? | +| [log](plugins/log) | Configurable logging. | ✅ | ✅ | ✅ | ✅ | ✅ | +| [nfc](plugins/nfc) | Read and write NFC tags on Android and iOS. | ? | ? | ? | ✅ | ✅ | +| [notification](plugins/notification) | Send message notifications (brief auto-expiring OS window element) to your user. Can also be used with the Notification Web API. | ✅ | ✅ | ✅ | ✅ | ✅ | +| [opener](plugins/opener) | Open files and URLs using their default application. | ✅ | ✅ | ✅ | ✅ | ✅ | +| [os](plugins/os) | Read information about the operating system. | ✅ | ✅ | ✅ | ✅ | ✅ | +| [persisted-scope](plugins/persisted-scope) | Persist runtime scope changes on the filesystem. | ✅ | ✅ | ✅ | ? | ? | +| [positioner](plugins/positioner) | Move windows to common locations. | ✅ | ✅ | ✅ | ❌ | ❌ | +| [process](plugins/process) | This plugin provides APIs to access the current process. To spawn child processes, see the [`shell`](https://github.com/tauri-apps/tauri-plugin-shell) plugin. | ✅ | ✅ | ✅ | ? | ? | +| [shell](plugins/shell) | Access the system shell. Allows you to spawn child processes and manage files and URLs using their default application. | ✅ | ✅ | ✅ | ? | ? | +| [single-instance](plugins/single-instance) | Ensure a single instance of your tauri app is running. | ✅ | ✅ | ✅ | ❌ | ❌ | +| [sql](plugins/sql) | Interface with SQL databases. | ✅ | ✅ | ✅ | ✅ | ✅ | +| [store](plugins/store) | Persistent key value storage. | ✅ | ✅ | ✅ | ✅ | ✅ | +| [stronghold](plugins/stronghold) | Encrypted, secure database. | ✅ | ✅ | ✅ | ? | ? | +| [updater](plugins/updater) | In-app updates for Tauri applications. | ✅ | ✅ | ✅ | ❌ | ❌ | +| [upload](plugins/upload) | Tauri plugin for file uploads through HTTP. | ✅ | ✅ | ✅ | ✅ | ✅ | +| [websocket](plugins/websocket) | Open a WebSocket connection using a Rust client in JS. | ✅ | ✅ | ✅ | ? | ? | +| [window-state](plugins/window-state) | Persist window sizes and positions. | ✅ | ✅ | ✅ | ❌ | ❌ | + +- ✅: (Partially) Supported +- ❌: Not supported +- `?` : Unknown/Untested or Planned + +## Contributing + +PRs accepted. Please make sure to read the [Contributing Guide](https://github.com/tauri-apps/tauri/blob/dev/.github/CONTRIBUTING.md) before making a pull request. + +## Partners + + + + + + + +
+ + CrabNebula + +
+ +For the complete list of sponsors please visit our [website](https://tauri.app#sponsors) and [Open Collective](https://opencollective.com/tauri). diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..4f09bba --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,23 @@ +# Security Policy + +**Do not report security vulnerabilities through public GitHub issues.** + +**Please use the [Private Vulnerability Disclosure](https://docs.github.com/en/code-security/security-advisories/guidance-on-reporting-and-writing-information-about-vulnerabilities/privately-reporting-a-security-vulnerability#privately-reporting-a-security-vulnerability) feature of GitHub.** + +Include as much of the following information: + +- Type of issue (e.g. improper input parsing, privilege escalation, etc.) +- The location of the affected source code (tag/branch/commit or direct URL) +- Any special configuration required to reproduce the issue +- The distribution affected or used to help us with reproduction of the issue +- Step-by-step instructions to reproduce the issue +- Ideally a reproduction repository +- Impact of the issue, including how an attacker might exploit the issue + +We prefer to receive reports in English. + +## Contact + +Please disclose a vulnerability or security relevant issue here: [https://github.com/tauri-apps/plugins-workspace/security/advisories/new](https://github.com/tauri-apps/plugins-workspace/security/advisories/new). + +Alternatively, you can also contact us by email via [security@tauri.app](mailto:security@tauri.app). diff --git a/eslint.config.js b/eslint.config.js new file mode 100644 index 0000000..2500c68 --- /dev/null +++ b/eslint.config.js @@ -0,0 +1,39 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +import eslint from '@eslint/js' +import { defineConfig } from 'eslint/config' +import eslintConfigPrettier from 'eslint-config-prettier' +import eslintPluginSecurity from 'eslint-plugin-security' +import tseslint from 'typescript-eslint' + +export default defineConfig( + { + ignores: [ + '**/target', + '**/node_modules', + '**/examples', + '**/dist', + '**/dist-js', + '**/build', + '**/api-iife.js', + '**/init-iife.js', + '**/init.js', + '**/rollup.config.js', + '**/bindings.ts', + '**/.test-server', + '.scripts', + 'eslint.config.js' + ] + }, + eslint.configs.recommended, + eslintConfigPrettier, + eslintPluginSecurity.configs.recommended, + ...tseslint.configs.recommendedTypeChecked, + { + languageOptions: { + parserOptions: { project: true, tsconfigRootDir: import.meta.dirname } + } + } +) diff --git a/examples/api/.setup-cross.sh b/examples/api/.setup-cross.sh new file mode 100644 index 0000000..a9e4f86 --- /dev/null +++ b/examples/api/.setup-cross.sh @@ -0,0 +1,8 @@ +#!/usr/bin/env bash +# Copyright 2019-2023 Tauri Programme within The Commons Conservancy +# SPDX-License-Identifier: Apache-2.0 +# SPDX-License-Identifier: MIT + +export ICONS_VOLUME="$(realpath icons)" +export DIST_VOLUME="$(realpath dist)" +export ISOLATION_VOLUME="$(realpath isolation-dist)" diff --git a/examples/api/.taurignore b/examples/api/.taurignore new file mode 100644 index 0000000..848a0f2 --- /dev/null +++ b/examples/api/.taurignore @@ -0,0 +1,3 @@ +src-tauri/locales/ +src-tauri/Cross.toml +src-tauri/.gitignore diff --git a/examples/api/CHANGELOG.md b/examples/api/CHANGELOG.md new file mode 100644 index 0000000..77b1396 --- /dev/null +++ b/examples/api/CHANGELOG.md @@ -0,0 +1,836 @@ +# Changelog + +## [2.0.45] + +### Dependencies + +- Upgraded to `log-js@2.9.2` +- Upgraded to `updater-js@2.12.0` + +## [2.0.44] + +### Dependencies + +- Upgraded to `http-js@2.6.1` +- Upgraded to `store-js@2.4.5` + +## [2.0.43] + +### Dependencies + +- Upgraded to `barcode-scanner-js@2.4.6` +- Upgraded to `biometric-js@2.3.3` +- Upgraded to `clipboard-manager-js@2.3.3` +- Upgraded to `dialog-js@2.7.3` +- Upgraded to `fs-js@2.5.2` +- Upgraded to `nfc-js@2.3.6` +- Upgraded to `notification-js@2.4.0` +- Upgraded to `opener-js@2.5.5` +- Upgraded to `shell-js@2.3.6` +- Upgraded to `http-js@2.6.0` +- Upgraded to `log-js@2.9.1` +- Upgraded to `updater-js@2.11.0` + +## \[2.0.42] + +### Dependencies + +- Upgraded to `dialog-js@2.7.2` +- Upgraded to `store-js@2.4.4` + +## \[2.0.41] + +### Dependencies + +- Upgraded to `log-js@2.9.0` + +## \[2.0.40] + +### Dependencies + +- Upgraded to `barcode-scanner-js@2.4.5` +- Upgraded to `global-shortcut-js@2.3.2` + +## \[2.0.39] + +### Dependencies + +- Upgraded to `fs-js@2.5.1` +- Upgraded to `opener-js@2.5.4` +- Upgraded to `store-js@2.4.3` +- Upgraded to `dialog-js@2.7.1` +- Upgraded to `http-js@2.5.9` + +## \[2.0.38] + +### Dependencies + +- Upgraded to `fs-js@2.5.0` +- Upgraded to `http-js@2.5.8` +- Upgraded to `updater-js@2.10.1` +- Upgraded to `nfc-js@2.3.5` +- Upgraded to `dialog-js@2.7.0` + +## \[2.0.37] + +### Dependencies + +- Upgraded to `updater-js@2.10.0` +- Upgraded to `barcode-scanner-js@2.4.4` +- Upgraded to `http-js@2.5.7` +- Upgraded to `shell-js@2.3.5` + +## \[2.0.36] + +### Dependencies + +- Upgraded to `http-js@2.5.6` +- Upgraded to `dialog-js@2.6.0` + +## \[2.0.35] + +### Dependencies + +- Upgraded to `dialog-js@2.5.0` +- Upgraded to `log-js@2.8.0` +- Upgraded to `shell-js@2.3.4` +- Upgraded to `barcode-scanner-js@2.4.3` +- Upgraded to `fs-js@2.4.5` +- Upgraded to `http-js@2.5.5` +- Upgraded to `nfc-js@2.3.4` +- Upgraded to `opener-js@2.5.3` +- Upgraded to `store-js@2.4.2` + +## \[2.0.34] + +### Dependencies + +- Upgraded to `barcode-scanner-js@2.4.2` +- Upgraded to `biometric-js@2.3.2` +- Upgraded to `cli-js@2.4.1` +- Upgraded to `clipboard-manager-js@2.3.2` +- Upgraded to `dialog-js@2.4.2` +- Upgraded to `fs-js@2.4.4` +- Upgraded to `global-shortcut-js@2.3.1` +- Upgraded to `http-js@2.5.4` +- Upgraded to `log-js@2.7.1` +- Upgraded to `nfc-js@2.3.3` +- Upgraded to `notification-js@2.3.3` +- Upgraded to `opener-js@2.5.2` +- Upgraded to `os-js@2.3.2` +- Upgraded to `process-js@2.3.1` +- Upgraded to `shell-js@2.3.3` +- Upgraded to `store-js@2.4.1` + +## \[2.0.33] + +### Dependencies + +- Upgraded to `barcode-scanner-js@2.4.1` +- Upgraded to `biometric-js@2.3.1` +- Upgraded to `clipboard-manager-js@2.3.1` +- Upgraded to `dialog-js@2.4.1` +- Upgraded to `fs-js@2.4.3` +- Upgraded to `nfc-js@2.3.2` +- Upgraded to `notification-js@2.3.2` +- Upgraded to `opener-js@2.5.1` +- Upgraded to `shell-js@2.3.2` +- Upgraded to `http-js@2.5.3` + +## \[2.0.32] + +### Dependencies + +- Upgraded to `dialog-js@2.4.0` +- Upgraded to `log-js@2.7.0` + +## \[2.0.31] + +### Dependencies + +- Upgraded to `shell-js@2.3.1` + +## \[2.0.30] + +### Dependencies + +- Upgraded to `notification-js@2.3.1` + +## \[2.0.29] + +### Dependencies + +- Upgraded to `fs-js@2.4.2` +- Upgraded to `nfc-js@2.3.1` +- Upgraded to `opener-js@2.5.0` +- Upgraded to `os-js@2.3.1` +- Upgraded to `store-js@2.4.0` +- Upgraded to `dialog-js@2.3.3` +- Upgraded to `http-js@2.5.2` + +## \[2.0.28] + +### Dependencies + +- Upgraded to `dialog-js@2.3.2` + +## \[2.0.27] + +### Dependencies + +- Upgraded to `barcode-scanner-js@2.4.0` +- Upgraded to `fs-js@2.4.1` +- Upgraded to `dialog-js@2.3.1` +- Upgraded to `http-js@2.5.1` + +## \[2.0.26] + +### Dependencies + +- Upgraded to `barcode-scanner-js@2.3.0` +- Upgraded to `biometric-js@2.3.0` +- Upgraded to `cli-js@2.4.0` +- Upgraded to `clipboard-manager-js@2.3.0` +- Upgraded to `fs-js@2.4.0` +- Upgraded to `dialog-js@2.3.0` +- Upgraded to `global-shortcut-js@2.3.0` +- Upgraded to `opener-js@2.4.0` +- Upgraded to `http-js@2.5.0` +- Upgraded to `log-js@2.6.0` +- Upgraded to `nfc-js@2.3.0` +- Upgraded to `notification-js@2.3.0` +- Upgraded to `os-js@2.3.0` +- Upgraded to `process-js@2.3.0` +- Upgraded to `shell-js@2.3.0` +- Upgraded to `store-js@2.3.0` +- Upgraded to `updater-js@2.9.0` + +## \[2.0.25] + +### Dependencies + +- Upgraded to `cli-js@2.3.0` +- Upgraded to `log-js@2.5.1` +- Upgraded to `opener-js@2.3.1` + +## \[2.0.24] + +### Dependencies + +- Upgraded to `updater-js@2.8.1` + +## \[2.0.23] + +### Dependencies + +- Upgraded to `updater-js@2.8.0` +- Upgraded to `barcode-scanner-js@2.2.1` +- Upgraded to `biometric-js@2.2.2` +- Upgraded to `cli-js@2.2.1` +- Upgraded to `clipboard-manager-js@2.2.3` +- Upgraded to `nfc-js@2.2.1` +- Upgraded to `notification-js@2.2.3` +- Upgraded to `os-js@2.2.2` +- Upgraded to `process-js@2.2.2` +- Upgraded to `shell-js@2.2.2` +- Upgraded to `store-js@2.2.1` +- Upgraded to `log-js@2.5.0` +- Upgraded to `opener-js@2.3.0` + +## \[2.0.22] + +### Dependencies + +- Upgraded to `fs-js@2.3.0` +- Upgraded to `global-shortcut-js@2.2.1` +- Upgraded to `http-js@2.4.4` +- Upgraded to `opener-js@2.2.7` +- Upgraded to `dialog-js@2.2.2` + +## \[2.0.21] + +### Dependencies + +- Upgraded to `log-js@2.4.0` +- Upgraded to `biometric-js@2.2.1` +- Upgraded to `updater-js@2.7.1` + +## \[2.0.20] + +### Dependencies + +- Upgraded to `http-js@2.4.3` +- Upgraded to `shell-js@2.2.1` +- Upgraded to `fs-js@2.2.1` +- Upgraded to `process-js@2.2.1` +- Upgraded to `updater-js@2.7.0` +- Upgraded to `dialog-js@2.2.1` + +## \[2.0.19] + +### Dependencies + +- Upgraded to `http-js@2.4.2` +- Upgraded to `updater-js@2.6.1` + +## \[2.0.18] + +### Dependencies + +- Upgraded to `http-js@2.4.1` + +## \[2.0.17] + +### Dependencies + +- Upgraded to `log-js@2.3.1` + +## \[2.0.16] + +### Dependencies + +- Upgraded to `clipboard-manager-js@2.2.2` +- Upgraded to `notification-js@2.2.2` +- Upgraded to `os-js@2.2.1` +- Upgraded to `http-js@2.4.0` +- Upgraded to `log-js@2.3.0` +- Upgraded to `updater-js@2.6.0` + +## \[2.0.15] + +### Dependencies + +- Upgraded to `log-js@2.2.3` +- Upgraded to `opener-js@2.2.6` + +## \[2.0.14] + +### Dependencies + +- Upgraded to `log-js@2.2.2` +- Upgraded to `updater-js@2.5.1` + +## \[2.0.13] + +### Dependencies + +- Upgraded to `updater-js@2.5.0` + +## \[2.0.12] + +### Dependencies + +- Upgraded to `clipboard-manager-js@2.2.1` +- Upgraded to `http-js@2.3.0` +- Upgraded to `log-js@2.2.1` +- Upgraded to `updater-js@2.4.0` + +## \[2.0.11] + +### Dependencies + +- Upgraded to `opener-js@2.2.5` + +## \[2.0.10] + +### Dependencies + +- Upgraded to `notification-js@2.2.1` +- Upgraded to `opener-js@2.2.4` + +## \[2.0.9] + +### Dependencies + +- Upgraded to `opener-js@2.2.3` +- Upgraded to `updater-js@2.3.1` + +## \[2.0.8] + +### Dependencies + +- Upgraded to `opener-js@2.2.2` + +## \[2.0.7] + +### Dependencies + +- Upgraded to `updater-js@2.3.0` +- Upgraded to `opener-js@2.2.1` + +## \[2.0.6] + +### Dependencies + +- Upgraded to `barcode-scanner-js@2.1.0` +- Upgraded to `biometric-js@2.1.0` +- Upgraded to `cli-js@2.1.0` +- Upgraded to `clipboard-manager-js@2.1.0` +- Upgraded to `dialog-js@2.1.0` +- Upgraded to `fs-js@2.1.0` +- Upgraded to `global-shortcut-js@2.1.0` +- Upgraded to `http-js@2.1.0` +- Upgraded to `log-js@2.1.0` +- Upgraded to `nfc-js@2.1.0` +- Upgraded to `notification-js@2.1.0` +- Upgraded to `opener-js@2.1.0` +- Upgraded to `os-js@2.1.0` +- Upgraded to `process-js@2.1.0` +- Upgraded to `shell-js@2.1.0` +- Upgraded to `store-js@2.2.0` +- Upgraded to `updater-js@2.1.0` + +## \[2.0.5] + +### Dependencies + +- Upgraded to `fs-js@2.0.4` +- Upgraded to `dialog-js@2.0.2` +- Upgraded to `http-js@2.0.2` + +## \[2.0.4] + +### Dependencies + +- Upgraded to `log-js@2.0.2` + +## \[2.0.3] + +### Dependencies + +- Upgraded to `clipboard-manager-js@2.0.1` +- Upgraded to `log-js@2.0.1` +- Upgraded to `fs-js@2.0.3` +- Upgraded to `opener-js@2.0.0` + +## \[2.0.2] + +### Dependencies + +- Upgraded to `fs-js@2.0.2` + +## \[2.0.1] + +### Dependencies + +- Upgraded to `dialog-js@2.0.1` +- Upgraded to `fs-js@2.0.1` +- Upgraded to `http-js@2.0.1` +- Upgraded to `shell-js@2.0.1` +- Upgraded to `store-js@2.1.0` + +## \[2.0.0] + +- [`e2c4dfb6`](https://github.com/tauri-apps/plugins-workspace/commit/e2c4dfb6af43e5dd8d9ceba232c315f5febd55c1) Update to tauri v2 stable release. + +### Dependencies + +- Upgraded to `barcode-scanner-js@2.0.0` +- Upgraded to `biometric-js@2.0.0` +- Upgraded to `cli-js@2.0.0` +- Upgraded to `clipboard-manager-js@2.0.0` +- Upgraded to `fs-js@2.0.0` +- Upgraded to `dialog-js@2.0.0` +- Upgraded to `global-shortcut-js@2.0.0` +- Upgraded to `http-js@2.0.0` +- Upgraded to `log-js@2.0.0` +- Upgraded to `nfc-js@2.0.0` +- Upgraded to `notification-js@2.0.0` +- Upgraded to `os-js@2.0.0` +- Upgraded to `process-js@2.0.0` +- Upgraded to `shell-js@2.0.0` +- Upgraded to `store-js@2.0.0` +- Upgraded to `updater-js@2.0.0` + +## \[2.0.0-rc.5] + +### Dependencies + +- Upgraded to `store-js@2.0.0-rc.2` + +## \[2.0.0-rc.4] + +### Dependencies + +- Upgraded to `barcode-scanner-js@2.0.0-rc.2` +- Upgraded to `clipboard-manager-js@2.0.0-rc.2` + +## \[2.0.0-rc.3] + +### Dependencies + +- Upgraded to `updater-js@2.0.0-rc.2` + +## \[2.0.0-rc.2] + +### Dependencies + +- Upgraded to `barcode-scanner-js@2.0.0-rc.1` +- Upgraded to `notification-js@2.0.0-rc.1` +- Upgraded to `dialog-js@2.0.0-rc.1` +- Upgraded to `biometric-js@2.0.0-rc.1` +- Upgraded to `cli-js@2.0.0-rc.1` +- Upgraded to `clipboard-manager-js@2.0.0-rc.1` +- Upgraded to `fs-js@2.0.0-rc.2` +- Upgraded to `global-shortcut-js@2.0.0-rc.1` +- Upgraded to `http-js@2.0.0-rc.2` +- Upgraded to `log-js@2.0.0-rc.1` +- Upgraded to `nfc-js@2.0.0-rc.1` +- Upgraded to `os-js@2.0.0-rc.1` +- Upgraded to `process-js@2.0.0-rc.1` +- Upgraded to `shell-js@2.0.0-rc.1` +- Upgraded to `store-js@2.0.0-rc.1` +- Upgraded to `updater-js@2.0.0-rc.1` + +## \[2.0.0-rc.1] + +### Dependencies + +- Upgraded to `http-js@2.0.0-rc.1` +- Upgraded to `fs-js@2.0.0-rc.1` + +## \[2.0.0-rc.0] + +### Dependencies + +- Upgraded to `barcode-scanner-js@2.0.0-rc.0` +- Upgraded to `biometric-js@2.0.0-rc.0` +- Upgraded to `cli-js@2.0.0-rc.0` +- Upgraded to `clipboard-manager-js@2.0.0-rc.0` +- Upgraded to `dialog-js@2.0.0-rc.0` +- Upgraded to `fs-js@2.0.0-rc.0` +- Upgraded to `global-shortcut-js@2.0.0-rc.0` +- Upgraded to `http-js@2.0.0-rc.0` +- Upgraded to `log-js@2.0.0-rc.0` +- Upgraded to `nfc-js@2.0.0-rc.0` +- Upgraded to `notification-js@2.0.0-rc.0` +- Upgraded to `os-js@2.0.0-rc.0` +- Upgraded to `process-js@2.0.0-rc.0` +- Upgraded to `shell-js@2.0.0-rc.0` +- Upgraded to `updater-js@2.0.0-rc.0` + +## \[2.0.0-beta.12] + +### Dependencies + +- Upgraded to `barcode-scanner-js@2.0.0-beta.8` +- Upgraded to `biometric-js@2.0.0-beta.8` +- Upgraded to `cli-js@2.0.0-beta.8` +- Upgraded to `clipboard-manager-js@2.1.0-beta.6` +- Upgraded to `dialog-js@2.0.0-beta.8` +- Upgraded to `fs-js@2.0.0-beta.8` +- Upgraded to `global-shortcut-js@2.0.0-beta.8` +- Upgraded to `http-js@2.0.0-beta.9` +- Upgraded to `log-js@2.0.0-beta.9` +- Upgraded to `nfc-js@2.0.0-beta.8` +- Upgraded to `notification-js@2.0.0-beta.8` +- Upgraded to `os-js@2.0.0-beta.8` +- Upgraded to `process-js@2.0.0-beta.8` +- Upgraded to `shell-js@2.0.0-beta.9` +- Upgraded to `updater-js@2.0.0-beta.8` + +## \[2.0.0-beta.11] + +### Dependencies + +- Upgraded to `global-shortcut-js@2.0.0-beta.7` +- Upgraded to `http-js@2.0.0-beta.8` +- Upgraded to `os-js@2.0.0-beta.7` +- Upgraded to `barcode-scanner-js@2.0.0-beta.7` +- Upgraded to `biometric-js@2.0.0-beta.7` +- Upgraded to `cli-js@2.0.0-beta.7` +- Upgraded to `clipboard-manager-js@2.1.0-beta.5` +- Upgraded to `dialog-js@2.0.0-beta.7` +- Upgraded to `fs-js@2.0.0-beta.7` +- Upgraded to `log-js@2.0.0-beta.8` +- Upgraded to `nfc-js@2.0.0-beta.7` +- Upgraded to `notification-js@2.0.0-beta.7` +- Upgraded to `process-js@2.0.0-beta.7` +- Upgraded to `shell-js@2.0.0-beta.8` +- Upgraded to `updater-js@2.0.0-beta.7` + +## \[2.0.0-beta.10] + +### Dependencies + +- Upgraded to `os-js@2.0.0-beta.6` +- Upgraded to `barcode-scanner-js@2.0.0-beta.6` +- Upgraded to `biometric-js@2.0.0-beta.6` +- Upgraded to `cli-js@2.0.0-beta.6` +- Upgraded to `clipboard-manager-js@2.1.0-beta.4` +- Upgraded to `dialog-js@2.0.0-beta.6` +- Upgraded to `fs-js@2.0.0-beta.6` +- Upgraded to `global-shortcut-js@2.0.0-beta.6` +- Upgraded to `http-js@2.0.0-beta.7` +- Upgraded to `log-js@2.0.0-beta.7` +- Upgraded to `nfc-js@2.0.0-beta.6` +- Upgraded to `notification-js@2.0.0-beta.6` +- Upgraded to `process-js@2.0.0-beta.6` +- Upgraded to `shell-js@2.0.0-beta.7` +- Upgraded to `updater-js@2.0.0-beta.6` + +## \[2.0.0-beta.9] + +### Dependencies + +- Upgraded to `http-js@2.0.0-beta.6` + +## \[2.0.0-beta.8] + +### Dependencies + +- Upgraded to `barcode-scanner-js@2.0.0-beta.5` +- Upgraded to `biometric-js@2.0.0-beta.5` +- Upgraded to `cli-js@2.0.0-beta.5` +- Upgraded to `clipboard-manager-js@2.1.0-beta.3` +- Upgraded to `dialog-js@2.0.0-beta.5` +- Upgraded to `fs-js@2.0.0-beta.5` +- Upgraded to `global-shortcut-js@2.0.0-beta.5` +- Upgraded to `http-js@2.0.0-beta.5` +- Upgraded to `log-js@2.0.0-beta.6` +- Upgraded to `nfc-js@2.0.0-beta.5` +- Upgraded to `notification-js@2.0.0-beta.5` +- Upgraded to `os-js@2.0.0-beta.5` +- Upgraded to `process-js@2.0.0-beta.5` +- Upgraded to `shell-js@2.0.0-beta.6` +- Upgraded to `updater-js@2.0.0-beta.5` + +## \[2.0.0-beta.7] + +### Dependencies + +- Upgraded to `http-js@2.0.0-beta.4` +- Upgraded to `barcode-scanner-js@2.0.0-beta.4` +- Upgraded to `biometric-js@2.0.0-beta.4` +- Upgraded to `cli-js@2.0.0-beta.4` +- Upgraded to `clipboard-manager-js@2.1.0-beta.2` +- Upgraded to `dialog-js@2.0.0-beta.4` +- Upgraded to `fs-js@2.0.0-beta.4` +- Upgraded to `global-shortcut-js@2.0.0-beta.4` +- Upgraded to `log-js@2.0.0-beta.5` +- Upgraded to `nfc-js@2.0.0-beta.4` +- Upgraded to `notification-js@2.0.0-beta.4` +- Upgraded to `os-js@2.0.0-beta.4` +- Upgraded to `process-js@2.0.0-beta.4` +- Upgraded to `shell-js@2.0.0-beta.5` +- Upgraded to `updater-js@2.0.0-beta.4` + +## \[2.0.0-beta.6] + +### Dependencies + +- Upgraded to `shell-js@2.0.0-beta.4` + +## \[2.0.0-beta.5] + +### Dependencies + +- Upgraded to `global-shortcut-js@2.0.0-beta.3` +- Upgraded to `barcode-scanner-js@2.0.0-beta.3` +- Upgraded to `biometric-js@2.0.0-beta.3` +- Upgraded to `cli-js@2.0.0-beta.3` +- Upgraded to `clipboard-manager-js@2.1.0-beta.1` +- Upgraded to `dialog-js@2.0.0-beta.3` +- Upgraded to `fs-js@2.0.0-beta.3` +- Upgraded to `http-js@2.0.0-beta.3` +- Upgraded to `log-js@2.0.0-beta.4` +- Upgraded to `nfc-js@2.0.0-beta.3` +- Upgraded to `notification-js@2.0.0-beta.3` +- Upgraded to `os-js@2.0.0-beta.3` +- Upgraded to `process-js@2.0.0-beta.3` +- Upgraded to `shell-js@2.0.0-beta.3` +- Upgraded to `updater-js@2.0.0-beta.3` + +## \[2.0.0-beta.4] + +### Dependencies + +- Upgraded to `log-js@2.0.0-beta.3` + +## \[2.0.0-beta.3] + +### Dependencies + +- Upgraded to `clipboard-manager-js@2.1.0-beta.0` + +## \[2.0.0-beta.2] + +### Dependencies + +- Upgraded to `clipboard-manager-js@2.0.0-beta.2` +- Upgraded to `dialog-js@2.0.0-beta.2` +- Upgraded to `fs-js@2.0.0-beta.2` +- Upgraded to `http-js@2.0.0-beta.2` +- Upgraded to `shell-js@2.0.0-beta.2` +- Upgraded to `barcode-scanner-js@2.0.0-beta.2` +- Upgraded to `biometric-js@2.0.0-beta.2` +- Upgraded to `cli-js@2.0.0-beta.2` +- Upgraded to `global-shortcut-js@2.0.0-beta.2` +- Upgraded to `log-js@2.0.0-beta.2` +- Upgraded to `nfc-js@2.0.0-beta.2` +- Upgraded to `notification-js@2.0.0-beta.2` +- Upgraded to `os-js@2.0.0-beta.2` +- Upgraded to `process-js@2.0.0-beta.2` +- Upgraded to `updater-js@2.0.0-beta.2` + +## \[2.0.0-beta.1] + +### Dependencies + +- Upgraded to `barcode-scanner-js@2.0.0-beta.1` +- Upgraded to `biometric-js@2.0.0-beta.1` +- Upgraded to `cli-js@2.0.0-beta.1` +- Upgraded to `clipboard-manager-js@2.0.0-beta.1` +- Upgraded to `dialog-js@2.0.0-beta.1` +- Upgraded to `fs-js@2.0.0-beta.1` +- Upgraded to `global-shortcut-js@2.0.0-beta.1` +- Upgraded to `http-js@2.0.0-beta.1` +- Upgraded to `log-js@2.0.0-beta.1` +- Upgraded to `nfc-js@2.0.0-beta.1` +- Upgraded to `notification-js@2.0.0-beta.1` +- Upgraded to `os-js@2.0.0-beta.1` +- Upgraded to `process-js@2.0.0-beta.1` +- Upgraded to `shell-js@2.0.0-beta.1` +- Upgraded to `updater-js@2.0.0-beta.1` + +## \[2.0.0-beta.0] + +### Dependencies + +- Upgraded to `barcode-scanner-js@2.0.0-beta.0` +- Upgraded to `biometric-js@2.0.0-beta.0` +- Upgraded to `cli-js@2.0.0-beta.0` +- Upgraded to `clipboard-manager-js@2.0.0-beta.0` +- Upgraded to `dialog-js@2.0.0-beta.0` +- Upgraded to `fs-js@2.0.0-beta.0` +- Upgraded to `global-shortcut-js@2.0.0-beta.0` +- Upgraded to `http-js@2.0.0-beta.0` +- Upgraded to `log-js@2.0.0-beta.0` +- Upgraded to `nfc-js@2.0.0-beta.0` +- Upgraded to `notification-js@2.0.0-beta.0` +- Upgraded to `os-js@2.0.0-beta.0` +- Upgraded to `process-js@2.0.0-beta.0` +- Upgraded to `shell-js@2.0.0-beta.0` +- Upgraded to `updater-js@2.0.0-beta.0` + +## \[2.0.0-alpha.9] + +### Dependencies + +- Upgraded to `fs-js@2.0.0-alpha.6` + +## \[2.0.0-alpha.8] + +### Dependencies + +- Upgraded to `http-js@2.0.0-alpha.6` + +## \[2.0.0-alpha.7] + +### Dependencies + +- Upgraded to `barcode-scanner-js@2.0.0-alpha.4` +- Upgraded to `cli-js@2.0.0-alpha.5` +- Upgraded to `clipboard-manager-js@2.0.0-alpha.5` +- Upgraded to `dialog-js@2.0.0-alpha.5` +- Upgraded to `fs-js@2.0.0-alpha.5` +- Upgraded to `global-shortcut-js@2.0.0-alpha.5` +- Upgraded to `http-js@2.0.0-alpha.5` +- Upgraded to `log-js@2.0.0-alpha.5` +- Upgraded to `notification-js@2.0.0-alpha.5` +- Upgraded to `os-js@2.0.0-alpha.6` +- Upgraded to `process-js@2.0.0-alpha.5` +- Upgraded to `shell-js@2.0.0-alpha.5` +- Upgraded to `updater-js@2.0.0-alpha.5` +- Upgraded to `biometric-js@2.0.0-alpha.0` +- Upgraded to `nfc-js@2.0.0-alpha.0` + +## \[2.0.0-alpha.6] + +### Dependencies + +- Upgraded to `barcode-scanner-js@2.0.0-alpha.3` +- Upgraded to `cli-js@2.0.0-alpha.4` +- Upgraded to `clipboard-manager-js@2.0.0-alpha.4` +- Upgraded to `dialog-js@2.0.0-alpha.4` +- Upgraded to `fs-js@2.0.0-alpha.4` +- Upgraded to `global-shortcut-js@2.0.0-alpha.4` +- Upgraded to `http-js@2.0.0-alpha.4` +- Upgraded to `log-js@2.0.0-alpha.4` +- Upgraded to `notification-js@2.0.0-alpha.4` +- Upgraded to `os-js@2.0.0-alpha.5` +- Upgraded to `process-js@2.0.0-alpha.4` +- Upgraded to `shell-js@2.0.0-alpha.4` +- Upgraded to `updater-js@2.0.0-alpha.4` + +## \[2.0.0-alpha.5] + +### Dependencies + +- Upgraded to `barcode-scanner-js@2.0.0-alpha.2` +- Upgraded to `cli-js@2.0.0-alpha.3` +- Upgraded to `clipboard-manager-js@2.0.0-alpha.3` +- Upgraded to `dialog-js@2.0.0-alpha.3` +- Upgraded to `fs-js@2.0.0-alpha.3` +- Upgraded to `global-shortcut-js@2.0.0-alpha.3` +- Upgraded to `http-js@2.0.0-alpha.3` +- Upgraded to `log-js@2.0.0-alpha.3` +- Upgraded to `notification-js@2.0.0-alpha.3` +- Upgraded to `os-js@2.0.0-alpha.4` +- Upgraded to `process-js@2.0.0-alpha.3` +- Upgraded to `shell-js@2.0.0-alpha.3` +- Upgraded to `updater-js@2.0.0-alpha.3` + +## \[2.0.0-alpha.4] + +### Dependencies + +- Upgraded to `barcode-scanner-js@2.0.0-alpha.1` +- Upgraded to `cli-js@2.0.0-alpha.2` +- Upgraded to `clipboard-manager-js@2.0.0-alpha.2` +- Upgraded to `dialog-js@2.0.0-alpha.2` +- Upgraded to `fs-js@2.0.0-alpha.2` +- Upgraded to `global-shortcut-js@2.0.0-alpha.2` +- Upgraded to `http-js@2.0.0-alpha.2` +- Upgraded to `log-js@2.0.0-alpha.2` +- Upgraded to `notification-js@2.0.0-alpha.2` +- Upgraded to `os-js@2.0.0-alpha.3` +- Upgraded to `process-js@2.0.0-alpha.2` +- Upgraded to `shell-js@2.0.0-alpha.2` +- Upgraded to `updater-js@2.0.0-alpha.2` + +## \[2.0.0-alpha.3] + +### Dependencies + +- Upgraded to `barcode-scanner-js@2.0.0-alpha.0` + +## \[2.0.0-alpha.2] + +### Dependencies + +- Upgraded to `os-js@2.0.0-alpha.2` + +## \[2.0.0-alpha.1] + +### Dependencies + +- Upgraded to `window-js@2.0.0-alpha.1` +- Upgraded to `fs-js@2.0.0-alpha.1` +- Upgraded to `http-js@2.0.0-alpha.1` +- Upgraded to `os-js@2.0.0-alpha.1` +- Upgraded to `app-js@2.0.0-alpha.1` +- Upgraded to `cli-js@2.0.0-alpha.1` +- Upgraded to `dialog-js@2.0.0-alpha.1` +- Upgraded to `global-shortcut-js@2.0.0-alpha.1` +- Upgraded to `log-js@2.0.0-alpha.1` +- Upgraded to `notification-js@2.0.0-alpha.1` +- Upgraded to `process-js@2.0.0-alpha.1` +- Upgraded to `shell-js@2.0.0-alpha.1` +- Upgraded to `updater-js@2.0.0-alpha.1` + +## \[2.0.0-alpha.0] + +### Dependencies + +- Plugins v2 alpha. diff --git a/examples/api/README.md b/examples/api/README.md new file mode 100644 index 0000000..60de2fc --- /dev/null +++ b/examples/api/README.md @@ -0,0 +1,28 @@ +# API example + +This example demonstrates Tauri's API capabilities using the plugins from this repository. It's used as the main validation app, serving as the testbed of our development process. +In the future, this app will be used on Tauri's integration tests. + +![App screenshot](./screenshot.png?raw=true) + +## Running the example + +- Install dependencies and build packages (Run inside of the repository root) + +```bash +$ pnpm install +$ pnpm build +``` + +- Run the app in development mode (Run inside of this folder `examples/api/`) + +```bash +$ pnpm tauri dev +``` + +- Build an run the release app (Run inside of this folder `examples/api/`) + +```bash +$ pnpm tauri build +$ ./src-tauri/target/release/app +``` diff --git a/examples/api/index.html b/examples/api/index.html new file mode 100644 index 0000000..acd46f1 --- /dev/null +++ b/examples/api/index.html @@ -0,0 +1,17 @@ + + + + + + + API Example App + + + +

+ + + diff --git a/examples/api/isolation-dist/index.html b/examples/api/isolation-dist/index.html new file mode 100644 index 0000000..a97caa8 --- /dev/null +++ b/examples/api/isolation-dist/index.html @@ -0,0 +1,10 @@ + + + + + Isolation Secure Script + + + + + diff --git a/examples/api/isolation-dist/index.js b/examples/api/isolation-dist/index.js new file mode 100644 index 0000000..7e2df30 --- /dev/null +++ b/examples/api/isolation-dist/index.js @@ -0,0 +1,7 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +window.__TAURI_ISOLATION_HOOK__ = (payload) => { + return payload +} diff --git a/examples/api/package.json b/examples/api/package.json new file mode 100644 index 0000000..c788209 --- /dev/null +++ b/examples/api/package.json @@ -0,0 +1,51 @@ +{ + "name": "api", + "private": true, + "version": "2.0.45", + "type": "module", + "scripts": { + "dev": "vite --clearScreen false", + "build": "vite build", + "serve": "vite preview", + "tauri": "tauri" + }, + "dependencies": { + "@tauri-apps/api": "^2.11.0", + "@tauri-apps/plugin-autostart": "workspace:*", + "@tauri-apps/plugin-barcode-scanner": "workspace:*", + "@tauri-apps/plugin-biometric": "workspace:*", + "@tauri-apps/plugin-cli": "workspace:*", + "@tauri-apps/plugin-clipboard-manager": "workspace:*", + "@tauri-apps/plugin-deep-link": "workspace:*", + "@tauri-apps/plugin-dialog": "workspace:*", + "@tauri-apps/plugin-fs": "workspace:*", + "@tauri-apps/plugin-geolocation": "workspace:*", + "@tauri-apps/plugin-global-shortcut": "workspace:*", + "@tauri-apps/plugin-haptics": "workspace:*", + "@tauri-apps/plugin-http": "workspace:*", + "@tauri-apps/plugin-nfc": "workspace:*", + "@tauri-apps/plugin-notification": "workspace:*", + "@tauri-apps/plugin-opener": "workspace:*", + "@tauri-apps/plugin-os": "workspace:*", + "@tauri-apps/plugin-positioner": "workspace:*", + "@tauri-apps/plugin-process": "workspace:*", + "@tauri-apps/plugin-shell": "workspace:*", + "@tauri-apps/plugin-sql": "workspace:*", + "@tauri-apps/plugin-store": "workspace:*", + "@tauri-apps/plugin-stronghold": "workspace:*", + "@tauri-apps/plugin-updater": "workspace:*", + "@tauri-apps/plugin-upload": "workspace:*", + "@tauri-apps/plugin-websocket": "workspace:*", + "@zerodevx/svelte-json-view": "2.0.0" + }, + "devDependencies": { + "@iconify-json/codicon": "^1.2.49", + "@iconify-json/ph": "^1.2.2", + "@sveltejs/vite-plugin-svelte": "^7.0.0", + "@tauri-apps/cli": "2.11.4", + "@unocss/extractor-svelte": "^66.6.7", + "svelte": "^5.55.7", + "unocss": "^66.6.7", + "vite": "^8.0.16" + } +} diff --git a/examples/api/public/tauri_logo.png b/examples/api/public/tauri_logo.png new file mode 100644 index 0000000000000000000000000000000000000000..2c53b8c4bd721c7346831ba6be581d03a0a4d755 GIT binary patch literal 2810 zcmVwHth5sLepoh&l$aA>P7ref%n2|jz?>kS6AYZdW~EhRi)8HML~ZY2gfUsWoAfh zv0AP#5L6Pk7xQXFMZ9>wMV6^2xv-A2Cu9~C2k0EA9|nI5;o-eYY?&k>uopQpUXc~o zERaZ8f%_x5t!Zwct&9=h0pg{G_88R+Tpzu{sSBiYqe7G+t{t*WEwP0KK@`G3UC}5j zutqy3K`D;sQcCQjUtYOGQ?A>9etL-|td^D01?oY{V2_4G9%puO86`y%j~25@#A=xw z;*AP}Jk|I`#kBgvg+^hQxlNX-WT3?gM}Zu$yWyr*_FRw$QH&)HQk;W2=W5DjYqh42 zbc}sC23lh4grK@6UYs1$m|@{STw9^L?FQ!!MxZsaOcim2<=utV;6PpeBU=kn5`Sqx}+uV*|KNaFBnCp#6~a~4Y$c7$3gRgnvrKiygYeKmZ>7Pu%5m- zMRJpNNs5egsQ6*+sa7QVe3#f0FW;B6{aUfs$4En(T=D$mf0TfWi;JS;I2$wun$70X z;^JaC>3M_@TMr&Q@Mr*^KYuP^-lUw-Mo2K4a;3acY1`rSnd|0;X>{4dI~7h}GQ8IYe+X7+{5ix>4H zO2F5zU#s|Xax@0;MO7e>EyX!poa5ZkwG43#9N^&Q@G;!}?dYq$KZbMH&~H7?@#En8 z{@$ZUk4lv68uaM~c6m?Uo|Pdg4ybe8Z8}i}q{J&SkrKPccAgH3Z0mg@CIx)yIa52n z^a4#esZ?s0g~=jH(oZR@pWd9i>Fb-SP`NWn<{-X5_>Q)_G$X)e81rnfbW5K;eag}F z!VQ7rlJ{5#d@WAf~PyAToya^zO&-^Ox&OPY{c7OhrUzHS8 zBSc|q<}MdjN~#QRMWJ-D+O3y@CA?RmZxyx%pYKf2>)7 z&>c?XggW9z7gn`1DB=OZy?gia*tXBswtxTdv&5?S?GaY$$}ed}MRa&WH#^?ipFJ|+ z%bhTbt^!$mGEV`iTqT#@B;?`4hb)qaP{M#SA3cR=r~-5YDsdj-IQhQg2JR36aE&IM z$B!SgH>n_|`T_q(M#1=evAtNw6+U@)mKVB`7(u18zpre1&jyH!f<$K>BR?J{_wU;X zM}*pA1e4vW(TgvCr3vEt`nrFiNB}He8A7V{Z%?Pwv0ca{aCLR1v`YMtuvMcNbcE$d z+E*ToWbtfmCDBAP#B<0Z;#&4#Du`f0TiN>oZje;*NPW`g3Nac~J%Ao8-Z5LP{sy?^ zeB2u{D+I1^<;{NzN*4%Ab-MQB0S!TDnJSbBQH*ZGg!hD@%8yCex=w!Dk+|$}BmAV# z&CPLwGEpw>d%9=+NW>1{wYJYRv*?@23iW9?h(RS z4c_?v@#*s)6N{j$IyD-Dh@`Ic_A$A^Had5RZ7j;6LW>EYj)y0<_i&_o0<~u9#Q847 zWfh+8k<=^)uar1>`u2365@0NOj!_M9*I+W2V-L4~d)p1R(YZq+(#9?mP8|=+W|9tw zu~d00acJrk$f%&fQfToI}#|^eojSfL>+D3=?93_Zq9S@6_ zq+gnv!X?q0>UR7qZHY=4lbN#4Mfc=n&&y_&u>`DC9uv{qWU4%9${ka_>wCdz14aDd zhSdYHm@s$fwg2E5301b8XD^0{3mu3maZ+gH0@?F%O(sX&As5!EcEqU7Btl&_f2dJ3L{`(pg)VY%iC0TGNMc1qwEX_R+9yTI z=D_I{A*5$$R;jY2>c)Io=vHG_a{YoAox+gLl!IV#HVd|vit2&r6(ZeA6e!l|m zbs2e{Ft0*fuh|vz>+dKHz@~|5g$o%}gXX=X^T~Xw|avVRk zOIG-XLOeB1DK(3$05KK#4Xm5)R2&agl8)XKu+_>bwzJhbNHC=nt&f6Wj8Ew4^W}O< z`s=VVoGV6@k@Iovgmdw#7nhRZnQ~3K#T5O8erEtYcoYFV#CB`Hn-FB~-+tIeGPNp&e0^7<8U}WQs_nRuu__h#CTK=|E(@u9!{Xv1 zXSmndrBMv{Mu!Q^>@Y5{(P;c%wcTX3kcTrbhH3B1|2%znx(f?cinNZ=YO!VFY&`}G z8D5*%IfW1f(;CIw05@U-u}Z{(s50n$^C7O)IQ!~=)p8MwMb`D1=H9puEZS&{9hHRTsjbA?d98QDpeJG{=f+HtA&E zZ|2ems{9nTxh$?H@6Hm}Z9jSU?>t*7Rds5sH0N$zpylr9nNeN&=Xpjt8tXzq8lvLM z)ZQT1s9KiadmQTmXPwAYeNg`{gn8Ag;M|v}iKhG&V)LZaTymOXNjhRxNSTi$PW!w2 zGCNFBpk;bc)}Y1`*Dzo(0Pz*A3!Hs~u7TYmsitj)gx#Ox)26jle+;z0Hq$e0Gb)(w z@$Y}O?(YwTh_lmAZ~ldpL26gFe)K;c{5uZ<6WI=xjnxj8nIx95+5`fl6f)?}2CR@v zP0T-uu)Zqz!4UGK6m_I0YS(jLjwX4k#$vHpEEbE!VzF2(7Rzn%8+s-;M1& literal 0 HcmV?d00001 diff --git a/examples/api/src-tauri/.gitignore b/examples/api/src-tauri/.gitignore new file mode 100644 index 0000000..99cb2b7 --- /dev/null +++ b/examples/api/src-tauri/.gitignore @@ -0,0 +1,10 @@ +# Generated by Cargo +# will have compiled files and executables +/target/ + +# cargo-mobile +.cargo/ + +gen/schemas/*.json +!gen/schemas/desktop-schema.json +!gen/schemas/mobile-schema.json diff --git a/examples/api/src-tauri/.taurignore b/examples/api/src-tauri/.taurignore new file mode 100644 index 0000000..cbff529 --- /dev/null +++ b/examples/api/src-tauri/.taurignore @@ -0,0 +1 @@ +tauri-plugin-sample/ \ No newline at end of file diff --git a/examples/api/src-tauri/CHANGELOG.md b/examples/api/src-tauri/CHANGELOG.md new file mode 100644 index 0000000..cb78a36 --- /dev/null +++ b/examples/api/src-tauri/CHANGELOG.md @@ -0,0 +1,1037 @@ +# Changelog + +## [2.0.49] + +### Dependencies + +- Upgraded to `http@2.7.0` +- Upgraded to `log@2.9.2` +- Upgraded to `updater@2.12.0` + +## [2.0.48] + +### Dependencies + +- Upgraded to `http@2.6.1` +- Upgraded to `store@2.4.5` + +## [2.0.47] + +### Dependencies + +- Upgraded to `barcode-scanner@2.4.6` +- Upgraded to `biometric@2.3.3` +- Upgraded to `clipboard-manager@2.3.3` +- Upgraded to `dialog@2.7.3` +- Upgraded to `fs@2.5.2` +- Upgraded to `geolocation@2.3.3` +- Upgraded to `haptics@2.3.3` +- Upgraded to `nfc@2.3.6` +- Upgraded to `notification@2.4.0` +- Upgraded to `opener@2.5.5` +- Upgraded to `shell@2.3.6` +- Upgraded to `http@2.6.0` +- Upgraded to `log@2.9.1` +- Upgraded to `updater@2.11.0` + +## \[2.0.46] + +### Dependencies + +- Upgraded to `dialog@2.7.2` +- Upgraded to `store@2.4.4` + +## \[2.0.45] + +### Dependencies + +- Upgraded to `log@2.9.0` + +## \[2.0.44] + +### Dependencies + +- Upgraded to `barcode-scanner@2.4.5` +- Upgraded to `global-shortcut@2.3.2` + +## \[2.0.43] + +### Dependencies + +- Upgraded to `fs@2.5.1` +- Upgraded to `opener@2.5.4` +- Upgraded to `store@2.4.3` +- Upgraded to `dialog@2.7.1` +- Upgraded to `http@2.5.9` + +## \[2.0.42] + +### Dependencies + +- Upgraded to `fs@2.5.0` +- Upgraded to `http@2.5.8` +- Upgraded to `updater@2.10.1` +- Upgraded to `nfc@2.3.5` +- Upgraded to `dialog@2.7.0` + +## \[2.0.41] + +### Dependencies + +- Upgraded to `updater@2.10.0` +- Upgraded to `barcode-scanner@2.4.4` +- Upgraded to `http@2.5.7` +- Upgraded to `shell@2.3.5` + +## \[2.0.40] + +### Dependencies + +- Upgraded to `http@2.5.6` +- Upgraded to `dialog@2.6.0` + +## \[2.0.39] + +### Dependencies + +- Upgraded to `dialog@2.5.0` +- Upgraded to `log@2.8.0` +- Upgraded to `shell@2.3.4` +- Upgraded to `barcode-scanner@2.4.3` +- Upgraded to `fs@2.4.5` +- Upgraded to `http@2.5.5` +- Upgraded to `nfc@2.3.4` +- Upgraded to `opener@2.5.3` +- Upgraded to `store@2.4.2` + +## \[2.0.38] + +### Dependencies + +- Upgraded to `barcode-scanner@2.4.2` +- Upgraded to `biometric@2.3.2` +- Upgraded to `cli@2.4.1` +- Upgraded to `clipboard-manager@2.3.2` +- Upgraded to `dialog@2.4.2` +- Upgraded to `fs@2.4.4` +- Upgraded to `geolocation@2.3.2` +- Upgraded to `global-shortcut@2.3.1` +- Upgraded to `haptics@2.3.2` +- Upgraded to `http@2.5.4` +- Upgraded to `log@2.7.1` +- Upgraded to `nfc@2.3.3` +- Upgraded to `notification@2.3.3` +- Upgraded to `opener@2.5.2` +- Upgraded to `os@2.3.2` +- Upgraded to `process@2.3.1` +- Upgraded to `shell@2.3.3` +- Upgraded to `store@2.4.1` + +## \[2.0.37] + +### Dependencies + +- Upgraded to `barcode-scanner@2.4.1` +- Upgraded to `biometric@2.3.1` +- Upgraded to `clipboard-manager@2.3.1` +- Upgraded to `dialog@2.4.1` +- Upgraded to `fs@2.4.3` +- Upgraded to `geolocation@2.3.1` +- Upgraded to `haptics@2.3.1` +- Upgraded to `nfc@2.3.2` +- Upgraded to `notification@2.3.2` +- Upgraded to `opener@2.5.1` +- Upgraded to `shell@2.3.2` +- Upgraded to `http@2.5.3` + +## \[2.0.36] + +### Dependencies + +- Upgraded to `dialog@2.4.0` +- Upgraded to `log@2.7.0` + +## \[2.0.35] + +### Dependencies + +- Upgraded to `shell@2.3.1` + +## \[2.0.34] + +### Dependencies + +- Upgraded to `notification@2.3.1` + +## \[2.0.33] + +### Dependencies + +- Upgraded to `fs@2.4.2` +- Upgraded to `nfc@2.3.1` +- Upgraded to `opener@2.5.0` +- Upgraded to `os@2.3.1` +- Upgraded to `store@2.4.0` +- Upgraded to `dialog@2.3.3` +- Upgraded to `http@2.5.2` + +## \[2.0.32] + +### Dependencies + +- Upgraded to `dialog@2.3.2` + +## \[2.0.31] + +### Dependencies + +- Upgraded to `barcode-scanner@2.4.0` +- Upgraded to `fs@2.4.1` +- Upgraded to `dialog@2.3.1` +- Upgraded to `http@2.5.1` + +## \[2.0.30] + +### Dependencies + +- Upgraded to `barcode-scanner@2.3.0` +- Upgraded to `biometric@2.3.0` +- Upgraded to `cli@2.4.0` +- Upgraded to `clipboard-manager@2.3.0` +- Upgraded to `fs@2.4.0` +- Upgraded to `dialog@2.3.0` +- Upgraded to `geolocation@2.3.0` +- Upgraded to `global-shortcut@2.3.0` +- Upgraded to `opener@2.4.0` +- Upgraded to `haptics@2.3.0` +- Upgraded to `http@2.5.0` +- Upgraded to `log@2.6.0` +- Upgraded to `nfc@2.3.0` +- Upgraded to `notification@2.3.0` +- Upgraded to `os@2.3.0` +- Upgraded to `process@2.3.0` +- Upgraded to `shell@2.3.0` +- Upgraded to `store@2.3.0` +- Upgraded to `updater@2.9.0` + +## \[2.0.29] + +### Dependencies + +- Upgraded to `cli@2.3.0` +- Upgraded to `log@2.5.1` +- Upgraded to `opener@2.3.1` + +## \[2.0.28] + +### Dependencies + +- Upgraded to `updater@2.8.1` + +## \[2.0.27] + +### Dependencies + +- Upgraded to `updater@2.8.0` +- Upgraded to `barcode-scanner@2.2.1` +- Upgraded to `biometric@2.2.2` +- Upgraded to `cli@2.2.1` +- Upgraded to `clipboard-manager@2.2.3` +- Upgraded to `geolocation@2.2.5` +- Upgraded to `haptics@2.2.5` +- Upgraded to `nfc@2.2.1` +- Upgraded to `notification@2.2.3` +- Upgraded to `os@2.2.2` +- Upgraded to `process@2.2.2` +- Upgraded to `shell@2.2.2` +- Upgraded to `store@2.2.1` +- Upgraded to `log@2.5.0` +- Upgraded to `opener@2.3.0` + +## \[2.0.26] + +### Dependencies + +- Upgraded to `fs@2.3.0` +- Upgraded to `global-shortcut@2.2.1` +- Upgraded to `http@2.4.4` +- Upgraded to `opener@2.2.7` +- Upgraded to `dialog@2.2.2` + +## \[2.0.25] + +### Dependencies + +- Upgraded to `log@2.4.0` +- Upgraded to `biometric@2.2.1` +- Upgraded to `updater@2.7.1` + +## \[2.0.24] + +### Dependencies + +- Upgraded to `http@2.4.3` +- Upgraded to `shell@2.2.1` +- Upgraded to `fs@2.2.1` +- Upgraded to `process@2.2.1` +- Upgraded to `updater@2.7.0` +- Upgraded to `dialog@2.2.1` + +## \[2.0.23] + +### Dependencies + +- Upgraded to `http@2.4.2` +- Upgraded to `updater@2.6.1` + +## \[2.0.22] + +### Dependencies + +- Upgraded to `http@2.4.1` + +## \[2.0.21] + +### Dependencies + +- Upgraded to `log@2.3.1` + +## \[2.0.20] + +### Dependencies + +- Upgraded to `clipboard-manager@2.2.2` +- Upgraded to `geolocation@2.2.4` +- Upgraded to `haptics@2.2.4` +- Upgraded to `notification@2.2.2` +- Upgraded to `os@2.2.1` +- Upgraded to `http@2.4.0` +- Upgraded to `log@2.3.0` +- Upgraded to `updater@2.6.0` + +## \[2.0.19] + +### Dependencies + +- Upgraded to `log@2.2.3` +- Upgraded to `opener@2.2.6` + +## \[2.0.18] + +### Dependencies + +- Upgraded to `log@2.2.2` +- Upgraded to `updater@2.5.1` + +## \[2.0.17] + +### Dependencies + +- Upgraded to `updater@2.5.0` + +## \[2.0.16] + +### Dependencies + +- Upgraded to `clipboard-manager@2.2.1` +- Upgraded to `http@2.3.0` +- Upgraded to `log@2.2.1` +- Upgraded to `updater@2.4.0` + +## \[2.0.15] + +### Dependencies + +- Upgraded to `haptics@2.2.3` +- Upgraded to `geolocation@2.2.3` +- Upgraded to `opener@2.2.5` + +## \[2.0.14] + +### Dependencies + +- Upgraded to `geolocation@2.2.2` +- Upgraded to `haptics@2.2.2` +- Upgraded to `notification@2.2.1` +- Upgraded to `opener@2.2.4` + +## \[2.0.13] + +### Dependencies + +- Upgraded to `geolocation@2.2.1` +- Upgraded to `haptics@2.2.1` + +## \[2.0.12] + +### Dependencies + +- Upgraded to `opener@2.2.3` +- Upgraded to `updater@2.3.1` + +## \[2.0.11] + +### Dependencies + +- Upgraded to `opener@2.2.2` + +## \[2.0.10] + +### Dependencies + +- Upgraded to `updater@2.3.0` +- Upgraded to `opener@2.2.1` + +## \[2.0.9] + +### Dependencies + +- Upgraded to `barcode-scanner@2.1.0` +- Upgraded to `biometric@2.1.0` +- Upgraded to `cli@2.1.0` +- Upgraded to `clipboard-manager@2.1.0` +- Upgraded to `dialog@2.1.0` +- Upgraded to `fs@2.2.0` +- Upgraded to `geolocation@2.1.0` +- Upgraded to `global-shortcut@2.1.0` +- Upgraded to `haptics@2.1.0` +- Upgraded to `http@2.1.0` +- Upgraded to `log@2.1.0` +- Upgraded to `nfc@2.1.0` +- Upgraded to `notification@2.1.0` +- Upgraded to `opener@2.1.0` +- Upgraded to `os@2.1.0` +- Upgraded to `process@2.1.0` +- Upgraded to `shell@2.1.0` +- Upgraded to `store@2.2.0` +- Upgraded to `updater@2.2.0` + +## \[2.0.8] + +### Dependencies + +- Upgraded to `fs@2.1.1` +- Upgraded to `dialog@2.0.5` +- Upgraded to `http@2.0.5` + +## \[2.0.7] + +### Dependencies + +- Upgraded to `log@2.0.4` + +## \[2.0.6] + +### Dependencies + +- Upgraded to `fs@2.1.0` +- Upgraded to `updater@2.1.0` +- Upgraded to `dialog@2.0.4` +- Upgraded to `log-plugin@2.0.3` +- Upgraded to `http@2.0.4` +- Upgraded to `opener@2.0.0` + +## \[2.0.5] + +### Dependencies + +- Upgraded to `clipboard-manager@2.0.2` +- Upgraded to `log-plugin@2.0.2` + +## \[2.0.4] + +### Dependencies + +- Upgraded to `fs@2.0.3` +- Upgraded to `dialog@2.0.3` +- Upgraded to `http@2.0.3` + +## \[2.0.3] + +### Dependencies + +- Upgraded to `dialog@2.0.2` +- Upgraded to `fs@2.0.2` +- Upgraded to `http@2.0.2` +- Upgraded to `shell@2.0.2` +- Upgraded to `store@2.1.0` + +## \[2.0.2] + +- [`a1a82208`](https://github.com/tauri-apps/plugins-workspace/commit/a1a82208ed4ab87f83310be0dc95428aec9ab241) ([#1873](https://github.com/tauri-apps/plugins-workspace/pull/1873) by [@lucasfernog](https://github.com/tauri-apps/plugins-workspace/../../lucasfernog)) Downgrade MSRV to 1.77.2 to support Windows 7. + +### Dependencies + +- Upgraded to `barcode-scanner@2.0.1` +- Upgraded to `biometric@2.0.1` +- Upgraded to `cli@2.0.1` +- Upgraded to `clipboard-manager@2.0.1` +- Upgraded to `fs@2.0.1` +- Upgraded to `dialog@2.0.1` +- Upgraded to `geolocation@2.0.1` +- Upgraded to `global-shortcut@2.0.1` +- Upgraded to `haptics@2.0.1` +- Upgraded to `http@2.0.1` +- Upgraded to `log-plugin@2.0.1` +- Upgraded to `nfc@2.0.1` +- Upgraded to `notification@2.0.1` +- Upgraded to `os@2.0.1` +- Upgraded to `process@2.0.1` +- Upgraded to `shell@2.0.1` +- Upgraded to `store@2.0.1` +- Upgraded to `updater@2.0.2` + +## \[2.0.1] + +### Dependencies + +- Upgraded to `updater@2.0.1` + +## \[2.0.0] + +- [`e2c4dfb6`](https://github.com/tauri-apps/plugins-workspace/commit/e2c4dfb6af43e5dd8d9ceba232c315f5febd55c1) Update to tauri v2 stable release. + +### Dependencies + +- Upgraded to `barcode-scanner@2.0.0` +- Upgraded to `biometric@2.0.0` +- Upgraded to `cli@2.0.0` +- Upgraded to `clipboard-manager@2.0.0` +- Upgraded to `fs@2.0.0` +- Upgraded to `dialog@2.0.0` +- Upgraded to `global-shortcut@2.0.0` +- Upgraded to `http@2.0.0` +- Upgraded to `log-plugin@2.0.0` +- Upgraded to `nfc@2.0.0` +- Upgraded to `notification@2.0.0` +- Upgraded to `os@2.0.0` +- Upgraded to `process@2.0.0` +- Upgraded to `shell@2.0.0` +- Upgraded to `store@2.0.0` +- Upgraded to `updater@2.0.0` + +## \[2.0.0-rc.8] + +### Dependencies + +- Upgraded to `cli@2.0.0-rc.2` +- Upgraded to `dialog@2.0.0-rc.8` +- Upgraded to `fs@2.0.0-rc.6` +- Upgraded to `shell@2.0.0-rc.4` +- Upgraded to `store@2.0.0-rc.4` +- Upgraded to `updater@2.0.0-rc.4` +- Upgraded to `http@2.0.0-rc.6` + +## \[2.0.0-rc.7] + +### Dependencies + +- Upgraded to `clipboard-manager@2.0.0-rc.4` +- Upgraded to `fs@2.0.0-rc.5` +- Upgraded to `notification@2.0.0-rc.5` +- Upgraded to `dialog@2.0.0-rc.7` +- Upgraded to `http@2.0.0-rc.5` + +## \[2.0.0-rc.6] + +### Dependencies + +- Upgraded to `dialog@2.0.0-rc.6` +- Upgraded to `fs@2.0.0-rc.4` +- Upgraded to `http@2.0.0-rc.4` + +## \[2.0.0-rc.5] + +### Dependencies + +- Upgraded to `barcode-scanner@2.0.0-rc.4` +- Upgraded to `notification@2.0.0-rc.4` + +## \[2.0.0-rc.4] + +### Dependencies + +- Upgraded to `fs@2.0.0-rc.3` +- Upgraded to `dialog@2.0.0-rc.5` +- Upgraded to `updater@2.0.0-rc.3` +- Upgraded to `http@2.0.0-rc.3` + +## \[2.0.0-rc.3] + +### Dependencies + +- Upgraded to `fs@2.0.0-rc.2` +- Upgraded to `dialog@2.0.0-rc.4` +- Upgraded to `http@2.0.0-rc.2` + +## \[2.0.0-rc.2] + +### Dependencies + +- Upgraded to `barcode-scanner@2.0.0-rc.3` +- Upgraded to `notification@2.0.0-rc.3` +- Upgraded to `dialog@2.0.0-rc.3` +- Upgraded to `fs@2.0.0-rc.1` +- Upgraded to `global-shortcut@2.0.0-rc.2` +- Upgraded to `store@2.0.0-rc.3` +- Upgraded to `biometric@2.0.0-rc.3` +- Upgraded to `cli@2.0.0-rc.1` +- Upgraded to `clipboard-manager@2.0.0-rc.3` +- Upgraded to `http@2.0.0-rc.1` +- Upgraded to `log-plugin@2.0.0-rc.2` +- Upgraded to `nfc@2.0.0-rc.3` +- Upgraded to `os@2.0.0-rc.1` +- Upgraded to `process@2.0.0-rc.1` +- Upgraded to `shell@2.0.0-rc.3` +- Upgraded to `updater@2.0.0-rc.2` + +## \[2.0.0-rc.1] + +### Dependencies + +- Upgraded to `barcode-scanner@2.0.0-rc.2` +- Upgraded to `biometric@2.0.0-rc.2` +- Upgraded to `clipboard-manager@2.0.0-rc.2` +- Upgraded to `dialog@2.0.0-rc.2` +- Upgraded to `log-plugin@2.0.0-rc.1` +- Upgraded to `nfc@2.0.0-rc.2` +- Upgraded to `notification@2.0.0-rc.2` +- Upgraded to `shell@2.0.0-rc.2` + +## \[2.0.0-rc.0] + +### Dependencies + +- Upgraded to `dialog@2.0.0-rc.1` +- Upgraded to `updater@2.0.0-rc.1` +- Upgraded to `barcode-scanner@2.0.0-rc.1` +- Upgraded to `clipboard-manager@2.0.0-rc.1` +- Upgraded to `global-shortcut@2.0.0-rc.1` +- Upgraded to `biometric@2.0.0-rc.1` +- Upgraded to `nfc@2.0.0-rc.1` +- Upgraded to `notification@2.0.0-rc.1` +- Upgraded to `shell@2.0.0-rc.1` + +## \[2.0.0-beta.17] + +### Dependencies + +- Upgraded to `barcode-scanner@2.0.0-rc.0` +- Upgraded to `biometric@2.0.0-rc.0` +- Upgraded to `cli@2.0.0-rc.0` +- Upgraded to `clipboard-manager@2.0.0-rc.0` +- Upgraded to `dialog@2.0.0-rc.0` +- Upgraded to `fs@2.0.0-rc.0` +- Upgraded to `global-shortcut@2.0.0-rc.0` +- Upgraded to `http@2.0.0-rc.0` +- Upgraded to `log-plugin@2.0.0-rc.0` +- Upgraded to `nfc@2.0.0-rc.0` +- Upgraded to `notification@2.0.0-rc.0` +- Upgraded to `os@2.0.0-rc.0` +- Upgraded to `process@2.0.0-rc.0` +- Upgraded to `shell@2.0.0-rc.0` +- Upgraded to `updater@2.0.0-rc.0` + +## \[2.0.0-beta.16] + +### Dependencies + +- Upgraded to `fs@2.0.0-beta.12` +- Upgraded to `barcode-scanner@2.0.0-beta.10` +- Upgraded to `biometric@2.0.0-beta.9` +- Upgraded to `cli@2.0.0-beta.9` +- Upgraded to `clipboard-manager@2.1.0-beta.7` +- Upgraded to `dialog@2.0.0-beta.12` +- Upgraded to `global-shortcut@2.0.0-beta.9` +- Upgraded to `http@2.0.0-beta.13` +- Upgraded to `log-plugin@2.0.0-beta.10` +- Upgraded to `nfc@2.0.0-beta.9` +- Upgraded to `notification@2.0.0-beta.12` +- Upgraded to `os@2.0.0-beta.9` +- Upgraded to `process@2.0.0-beta.9` +- Upgraded to `shell@2.0.0-beta.10` +- Upgraded to `updater@2.0.0-beta.12` + +## \[2.0.0-beta.15] + +### Dependencies + +- Upgraded to `log-plugin@2.0.0-beta.9` + +## \[2.0.0-beta.14] + +### Dependencies + +- Upgraded to `notification@2.0.0-beta.11` +- Upgraded to `updater@2.0.0-beta.11` + +## \[2.0.0-beta.13] + +### Dependencies + +- Upgraded to `biometric@2.0.0-beta.8` +- Upgraded to `global-shortcut@2.0.0-beta.8` +- Upgraded to `http@2.0.0-beta.12` +- Upgraded to `barcode-scanner@2.0.0-beta.9` +- Upgraded to `cli@2.0.0-beta.8` +- Upgraded to `clipboard-manager@2.1.0-beta.6` +- Upgraded to `dialog@2.0.0-beta.11` +- Upgraded to `fs@2.0.0-beta.11` +- Upgraded to `log-plugin@2.0.0-beta.8` +- Upgraded to `nfc@2.0.0-beta.8` +- Upgraded to `notification@2.0.0-beta.10` +- Upgraded to `os@2.0.0-beta.8` +- Upgraded to `process@2.0.0-beta.8` +- Upgraded to `shell@2.0.0-beta.9` +- Upgraded to `updater@2.0.0-beta.10` + +## \[2.0.0-beta.12] + +### Dependencies + +- Upgraded to `clipboard-manager@2.1.0-beta.5` +- Upgraded to `fs@2.0.0-beta.10` +- Upgraded to `updater@2.0.0-beta.9` +- Upgraded to `notification@2.0.0-beta.9` +- Upgraded to `os@2.0.0-beta.7` +- Upgraded to `barcode-scanner@2.0.0-beta.8` +- Upgraded to `biometric@2.0.0-beta.7` +- Upgraded to `cli@2.0.0-beta.7` +- Upgraded to `dialog@2.0.0-beta.10` +- Upgraded to `global-shortcut@2.0.0-beta.7` +- Upgraded to `http@2.0.0-beta.11` +- Upgraded to `log-plugin@2.0.0-beta.7` +- Upgraded to `nfc@2.0.0-beta.7` +- Upgraded to `process@2.0.0-beta.7` +- Upgraded to `shell@2.0.0-beta.8` + +## \[2.0.0-beta.11] + +### Dependencies + +- Upgraded to `notification@2.0.0-beta.8` +- Upgraded to `http@2.0.0-beta.10` +- Upgraded to `updater@2.0.0-beta.8` + +## \[2.0.0-beta.10] + +### Dependencies + +- Upgraded to `barcode-scanner@2.0.0-beta.7` +- Upgraded to `biometric@2.0.0-beta.6` +- Upgraded to `clipboard-manager@2.1.0-beta.4` +- Upgraded to `nfc@2.0.0-beta.6` +- Upgraded to `notification@2.0.0-beta.7` +- Upgraded to `shell@2.0.0-beta.7` +- Upgraded to `cli@2.0.0-beta.6` +- Upgraded to `dialog@2.0.0-beta.9` +- Upgraded to `fs@2.0.0-beta.9` +- Upgraded to `global-shortcut@2.0.0-beta.6` +- Upgraded to `http@2.0.0-beta.9` +- Upgraded to `log-plugin@2.0.0-beta.6` +- Upgraded to `os@2.0.0-beta.6` +- Upgraded to `process@2.0.0-beta.6` +- Upgraded to `updater@2.0.0-beta.7` + +## \[2.0.0-beta.9] + +### Dependencies + +- Upgraded to `clipboard-manager@2.1.0-beta.3` +- Upgraded to `dialog@2.0.0-beta.8` +- Upgraded to `http@2.0.0-beta.8` +- Upgraded to `notification@2.0.0-beta.6` +- Upgraded to `shell@2.0.0-beta.6` +- Upgraded to `barcode-scanner@2.0.0-beta.6` +- Upgraded to `biometric@2.0.0-beta.5` +- Upgraded to `nfc@2.0.0-beta.5` +- Upgraded to `cli@2.0.0-beta.5` +- Upgraded to `fs@2.0.0-beta.8` +- Upgraded to `global-shortcut@2.0.0-beta.5` +- Upgraded to `log-plugin@2.0.0-beta.5` +- Upgraded to `os@2.0.0-beta.5` +- Upgraded to `process@2.0.0-beta.5` +- Upgraded to `updater@2.0.0-beta.6` + +## \[2.0.0-beta.8] + +### Dependencies + +- Upgraded to `shell@2.0.0-beta.5` + +## \[2.0.0-beta.7] + +### Dependencies + +- Upgraded to `clipboard-manager@2.1.0-beta.2` +- Upgraded to `global-shortcut@2.0.0-beta.4` +- Upgraded to `barcode-scanner@2.0.0-beta.5` +- Upgraded to `biometric@2.0.0-beta.4` +- Upgraded to `cli@2.0.0-beta.4` +- Upgraded to `dialog@2.0.0-beta.7` +- Upgraded to `fs@2.0.0-beta.7` +- Upgraded to `http@2.0.0-beta.7` +- Upgraded to `log-plugin@2.0.0-beta.4` +- Upgraded to `nfc@2.0.0-beta.4` +- Upgraded to `notification@2.0.0-beta.5` +- Upgraded to `os@2.0.0-beta.4` +- Upgraded to `process@2.0.0-beta.4` +- Upgraded to `shell@2.0.0-beta.4` +- Upgraded to `updater@2.0.0-beta.5` + +## \[2.0.0-beta.6] + +### Dependencies + +- Upgraded to `notification@2.0.0-beta.4` +- Upgraded to `barcode-scanner@2.0.0-beta.4` +- Upgraded to `dialog@2.0.0-beta.6` +- Upgraded to `fs@2.0.0-beta.6` +- Upgraded to `http@2.0.0-beta.6` + +## \[2.0.0-beta.5] + +### Dependencies + +- Upgraded to `clipboard-manager@2.1.0-beta.1` +- Upgraded to `http@2.0.0-beta.5` +- Upgraded to `updater@2.0.0-beta.4` +- Upgraded to `dialog@2.0.0-beta.5` +- Upgraded to `fs@2.0.0-beta.5` + +## \[2.0.0-beta.4] + +### Dependencies + +- Upgraded to `dialog@2.0.0-beta.4` +- Upgraded to `fs@2.0.0-beta.4` +- Upgraded to `http@2.0.0-beta.4` + +## \[2.0.0-beta.3] + +### Dependencies + +- Upgraded to `clipboard-manager@2.1.0-beta.0` +- Upgraded to `dialog@2.0.0-beta.3` +- Upgraded to `fs@2.0.0-beta.3` +- Upgraded to `http@2.0.0-beta.3` +- Upgraded to `updater@2.0.0-beta.3` +- Upgraded to `barcode-scanner@2.0.0-beta.3` +- Upgraded to `biometric@2.0.0-beta.3` +- Upgraded to `cli@2.0.0-beta.3` +- Upgraded to `global-shortcut@2.0.0-beta.3` +- Upgraded to `log-plugin@2.0.0-beta.3` +- Upgraded to `nfc@2.0.0-beta.3` +- Upgraded to `notification@2.0.0-beta.3` +- Upgraded to `os@2.0.0-beta.3` +- Upgraded to `process@2.0.0-beta.3` +- Upgraded to `shell@2.0.0-beta.3` + +## \[2.0.0-beta.2] + +### Dependencies + +- Upgraded to `clipboard-manager@2.0.0-beta.2` +- Upgraded to `dialog@2.0.0-beta.2` +- Upgraded to `http@2.0.0-beta.2` +- Upgraded to `fs@2.0.0-beta.2` +- Upgraded to `shell@2.0.0-beta.2` +- Upgraded to `barcode-scanner@2.0.0-beta.2` +- Upgraded to `biometric@2.0.0-beta.2` +- Upgraded to `cli@2.0.0-beta.2` +- Upgraded to `global-shortcut@2.0.0-beta.2` +- Upgraded to `log-plugin@2.0.0-beta.2` +- Upgraded to `nfc@2.0.0-beta.2` +- Upgraded to `notification@2.0.0-beta.2` +- Upgraded to `os@2.0.0-beta.2` +- Upgraded to `process@2.0.0-beta.2` +- Upgraded to `updater@2.0.0-beta.2` + +## \[2.0.0-beta.1] + +### Dependencies + +- Upgraded to `barcode-scanner@2.0.0-beta.1` +- Upgraded to `biometric@2.0.0-beta.1` +- Upgraded to `cli@2.0.0-beta.1` +- Upgraded to `clipboard-manager@2.0.0-beta.1` +- Upgraded to `dialog@2.0.0-beta.1` +- Upgraded to `fs@2.0.0-beta.1` +- Upgraded to `global-shortcut@2.0.0-beta.1` +- Upgraded to `http@2.0.0-beta.1` +- Upgraded to `log-plugin@2.0.0-beta.1` +- Upgraded to `nfc@2.0.0-beta.1` +- Upgraded to `notification@2.0.0-beta.1` +- Upgraded to `os@2.0.0-beta.1` +- Upgraded to `process@2.0.0-beta.1` +- Upgraded to `shell@2.0.0-beta.1` +- Upgraded to `updater@2.0.0-beta.1` + +## \[2.0.0-beta.0] + +### Dependencies + +- Upgraded to `barcode-scanner@2.0.0-beta.0` +- Upgraded to `biometric@2.0.0-beta.0` +- Upgraded to `cli@2.0.0-beta.0` +- Upgraded to `clipboard-manager@2.0.0-beta.0` +- Upgraded to `dialog@2.0.0-beta.0` +- Upgraded to `fs@2.0.0-beta.0` +- Upgraded to `global-shortcut@2.0.0-beta.0` +- Upgraded to `http@2.0.0-beta.0` +- Upgraded to `log@2.0.0-beta.0` +- Upgraded to `nfc@2.0.0-beta.0` +- Upgraded to `notification@2.0.0-beta.0` +- Upgraded to `os@2.0.0-beta.0` +- Upgraded to `process@2.0.0-beta.0` +- Upgraded to `shell@2.0.0-beta.0` +- Upgraded to `updater@2.0.0-beta.0` + +## \[2.0.0-alpha.11] + +### Dependencies + +- Upgraded to `fs@2.0.0-alpha.7` +- Upgraded to `dialog@2.0.0-alpha.7` +- Upgraded to `http@2.0.0-alpha.9` + +## \[2.0.0-alpha.10] + +### Dependencies + +- Upgraded to `http@2.0.0-alpha.8` + +## \[2.0.0-alpha.9] + +### Dependencies + +- Upgraded to `barcode-scanner@2.0.0-alpha.4` +- Upgraded to `cli@2.0.0-alpha.6` +- Upgraded to `clipboard-manager@2.0.0-alpha.6` +- Upgraded to `dialog@2.0.0-alpha.6` +- Upgraded to `fs@2.0.0-alpha.6` +- Upgraded to `global-shortcut@2.0.0-alpha.6` +- Upgraded to `http@2.0.0-alpha.7` +- Upgraded to `log-plugin@2.0.0-alpha.6` +- Upgraded to `notification@2.0.0-alpha.7` +- Upgraded to `os@2.0.0-alpha.6` +- Upgraded to `process@2.0.0-alpha.6` +- Upgraded to `shell@2.0.0-alpha.6` +- Upgraded to `updater@2.0.0-alpha.6` +- Upgraded to `biometric@2.0.0-alpha.0` +- Upgraded to `nfc@2.0.0-alpha.0` + +## \[2.0.0-alpha.8] + +### Dependencies + +- Upgraded to `barcode-scanner@2.0.0-alpha.3` +- Upgraded to `cli@2.0.0-alpha.5` +- Upgraded to `clipboard-manager@2.0.0-alpha.5` +- Upgraded to `dialog@2.0.0-alpha.5` +- Upgraded to `fs@2.0.0-alpha.5` +- Upgraded to `global-shortcut@2.0.0-alpha.5` +- Upgraded to `http@2.0.0-alpha.6` +- Upgraded to `log-plugin@2.0.0-alpha.5` +- Upgraded to `notification@2.0.0-alpha.6` +- Upgraded to `os@2.0.0-alpha.5` +- Upgraded to `process@2.0.0-alpha.5` +- Upgraded to `shell@2.0.0-alpha.5` +- Upgraded to `updater@2.0.0-alpha.5` + +## \[2.0.0-alpha.7] + +### Dependencies + +- Upgraded to `barcode-scanner@2.0.0-alpha.2` +- Upgraded to `cli@2.0.0-alpha.4` +- Upgraded to `clipboard-manager@2.0.0-alpha.4` +- Upgraded to `dialog@2.0.0-alpha.4` +- Upgraded to `fs@2.0.0-alpha.4` +- Upgraded to `global-shortcut@2.0.0-alpha.4` +- Upgraded to `http@2.0.0-alpha.5` +- Upgraded to `log-plugin@2.0.0-alpha.4` +- Upgraded to `notification@2.0.0-alpha.5` +- Upgraded to `os@2.0.0-alpha.4` +- Upgraded to `process@2.0.0-alpha.4` +- Upgraded to `shell@2.0.0-alpha.4` +- Upgraded to `updater@2.0.0-alpha.4` + +## \[2.0.0-alpha.6] + +### Dependencies + +- Upgraded to `log-plugin@2.0.0-alpha.3` +- Upgraded to `shell@2.0.0-alpha.3` +- Upgraded to `dialog@2.0.0-alpha.3` +- Upgraded to `notification@2.0.0-alpha.4` +- Upgraded to `updater@2.0.0-alpha.3` +- Upgraded to `global-shortcut@2.0.0-alpha.3` +- Upgraded to `barcode-scanner@2.0.0-alpha.1` +- Upgraded to `cli@2.0.0-alpha.3` +- Upgraded to `clipboard-manager@2.0.0-alpha.3` +- Upgraded to `fs@2.0.0-alpha.3` +- Upgraded to `http@2.0.0-alpha.4` +- Upgraded to `os@2.0.0-alpha.3` +- Upgraded to `process@2.0.0-alpha.3` + +## \[2.0.0-alpha.5] + +### Dependencies + +- Upgraded to `barcode-scanner@2.0.0-alpha.0` + +## \[2.0.0-alpha.4] + +### Dependencies + +- Upgraded to `http@2.0.0-alpha.3` +- Upgraded to `app@2.0.0-alpha.2` +- Upgraded to `cli@2.0.0-alpha.2` +- Upgraded to `clipboard-manager@2.0.0-alpha.2` +- Upgraded to `dialog@2.0.0-alpha.2` +- Upgraded to `fs@2.0.0-alpha.2` +- Upgraded to `global-shortcut@2.0.0-alpha.2` +- Upgraded to `log-plugin@2.0.0-alpha.2` +- Upgraded to `notification@2.0.0-alpha.3` +- Upgraded to `os@2.0.0-alpha.2` +- Upgraded to `process@2.0.0-alpha.2` +- Upgraded to `shell@2.0.0-alpha.2` +- Upgraded to `updater@2.0.0-alpha.2` +- Upgraded to `window@2.0.0-alpha.2` + +## \[2.0.0-alpha.3] + +### Dependencies + +- Upgraded to `http@2.0.0-alpha.2` + +## \[2.0.0-alpha.2] + +### Dependencies + +- Upgraded to `dialog@2.0.0-alpha.1` +- Upgraded to `window@2.0.0-alpha.1` +- Upgraded to `app@2.0.0-alpha.1` +- Upgraded to `cli@2.0.0-alpha.1` +- Upgraded to `fs@2.0.0-alpha.1` +- Upgraded to `global-shortcut@2.0.0-alpha.1` +- Upgraded to `http@2.0.0-alpha.1` +- Upgraded to `log-plugin@2.0.0-alpha.1` +- Upgraded to `notification@2.0.0-alpha.2` +- Upgraded to `os@2.0.0-alpha.1` +- Upgraded to `process@2.0.0-alpha.1` +- Upgraded to `shell@2.0.0-alpha.1` +- Upgraded to `updater@2.0.0-alpha.1` + +## \[2.0.0-alpha.1] + +### Dependencies + +- Updated to latest `notification` + +## \[2.0.0-alpha.0] + +### Dependencies + +- Plugins v2 alpha. diff --git a/examples/api/src-tauri/Cargo.toml b/examples/api/src-tauri/Cargo.toml new file mode 100644 index 0000000..db762fc --- /dev/null +++ b/examples/api/src-tauri/Cargo.toml @@ -0,0 +1,88 @@ +[package] +name = "api" +publish = false +version = "2.0.49" +description = "An example Tauri Application showcasing the api" +edition = "2021" +rust-version = { workspace = true } +license = "Apache-2.0 OR MIT" + +[lib] +name = "api_lib" +crate-type = ["staticlib", "cdylib", "rlib"] + +[build-dependencies] +tauri-build = { workspace = true, features = ["codegen", "isolation"] } + +[dependencies] +serde_json = { workspace = true } +serde = { workspace = true } +tiny_http = "0.12" +time = "0.3" +log = { workspace = true } +tauri-plugin-log = { path = "../../../plugins/log", version = "2.9.2" } +tauri-plugin-fs = { path = "../../../plugins/fs", version = "2.5.2", features = [ + "watch", +] } +tauri-plugin-clipboard-manager = { path = "../../../plugins/clipboard-manager", version = "2.3.3" } +tauri-plugin-dialog = { path = "../../../plugins/dialog", version = "2.7.3" } +tauri-plugin-http = { path = "../../../plugins/http", features = [ + "multipart", + "cookies", +], version = "2.7.0" } +tauri-plugin-notification = { path = "../../../plugins/notification", version = "2.4.0", features = [ + "windows7-compat", +] } +tauri-plugin-os = { path = "../../../plugins/os", version = "2.3.2" } +tauri-plugin-process = { path = "../../../plugins/process", version = "2.3.1" } +tauri-plugin-opener = { path = "../../../plugins/opener", version = "2.5.5" } +tauri-plugin-shell = { path = "../../../plugins/shell", version = "2.3.6" } +tauri-plugin-store = { path = "../../../plugins/store", version = "2.4.5" } +tauri-plugin-upload = { path = "../../../plugins/upload", version = "2.3.0" } +tauri-plugin-deep-link = { path = "../../../plugins/deep-link", version = "2.4.10" } +tauri-plugin-sql = { path = "../../../plugins/sql", version = "2.4.1", features = [ + "sqlite", +] } +tauri-plugin-stronghold = { path = "../../../plugins/stronghold", version = "2.3.2" } +tauri-plugin-websocket = { path = "../../../plugins/websocket", version = "2.4.3" } + +# WebDriver automation bridge, used by the plugins e2e suite (packages/api-e2e). +# Desktop-only and behind the off-by-default `automation` feature so it never ships in a +# regular build. +[target."cfg(not(any(target_os = \"android\", target_os = \"ios\")))".dependencies] +tauri-plugin-automation = { version = "0.2.0", optional = true } + +[dependencies.tauri] +workspace = true +features = [ + "wry", + "common-controls-v6", + "x11", + "image-ico", + "image-png", + "isolation", + "macos-private-api", + "tray-icon", + "protocol-asset", +] + +[target."cfg(any(target_os = \"macos\", windows, target_os = \"linux\", target_os = \"dragonfly\", target_os = \"freebsd\", target_os = \"openbsd\", target_os = \"netbsd\"))".dependencies] +tauri-plugin-autostart = { path = "../../../plugins/autostart", version = "2.5.1" } +tauri-plugin-cli = { path = "../../../plugins/cli", version = "2.4.1" } +tauri-plugin-global-shortcut = { path = "../../../plugins/global-shortcut", version = "2.3.2" } +tauri-plugin-positioner = { path = "../../../plugins/positioner", version = "2.3.4", features = [ + "tray-icon", +] } +tauri-plugin-updater = { path = "../../../plugins/updater", version = "2.12.0" } +tauri-plugin-window-state = { path = "../../../plugins/window-state", version = "2.2.0" } + +[target."cfg(any(target_os = \"android\", target_os = \"ios\"))".dependencies] +tauri-plugin-barcode-scanner = { path = "../../../plugins/barcode-scanner/", version = "2.4.6" } +tauri-plugin-nfc = { path = "../../../plugins/nfc", version = "2.3.6" } +tauri-plugin-biometric = { path = "../../../plugins/biometric/", version = "2.3.3" } +tauri-plugin-geolocation = { path = "../../../plugins/geolocation/", version = "2.3.3" } +tauri-plugin-haptics = { path = "../../../plugins/haptics/", version = "2.3.3" } + +[features] +# Enables the WebDriver automation bridge; set by the e2e suite's build (packages/api-e2e). +automation = ["dep:tauri-plugin-automation"] diff --git a/examples/api/src-tauri/Cross.toml b/examples/api/src-tauri/Cross.toml new file mode 100644 index 0000000..23ac27b --- /dev/null +++ b/examples/api/src-tauri/Cross.toml @@ -0,0 +1,11 @@ +[build.env] +# must set ICONS_VOLUME, DIST_VOLUME and ISOLATION_VOLUME environment variables +# ICONS_VOLUME: absolute path to the icons folder +# DIST_VOLUME: absolute path to the dist folder +# ISOLATION_VOLUME: absolute path to the isolation dist folder +# this can be done running `$ . .setup-cross.sh` in the examples/api folder +volumes = ["ICONS_VOLUME", "DIST_VOLUME", "ISOLATION_VOLUME"] + +[target.aarch64-unknown-linux-gnu] +image = "aarch64-unknown-linux-gnu:latest" +#image = "ghcr.io/tauri-apps/tauri/aarch64-unknown-linux-gnu:latest" diff --git a/examples/api/src-tauri/Info.plist b/examples/api/src-tauri/Info.plist new file mode 100644 index 0000000..4dcd6ac --- /dev/null +++ b/examples/api/src-tauri/Info.plist @@ -0,0 +1,14 @@ + + + + + NSCameraUsageDescription + Request camera access for WebRTC + NSMicrophoneUsageDescription + Request microphone access for WebRTC + NSFaceIDUsageDescription + Authenticate with biometrics + NFCReaderUsageDescription + Read and write to NFC tags for testing + + diff --git a/examples/api/src-tauri/build.rs b/examples/api/src-tauri/build.rs new file mode 100644 index 0000000..88537dd --- /dev/null +++ b/examples/api/src-tauri/build.rs @@ -0,0 +1,7 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +fn main() { + tauri_build::build(); +} diff --git a/examples/api/src-tauri/capabilities/base.json b/examples/api/src-tauri/capabilities/base.json new file mode 100644 index 0000000..90c6d97 --- /dev/null +++ b/examples/api/src-tauri/capabilities/base.json @@ -0,0 +1,148 @@ +{ + "$schema": "../gen/schemas/desktop-schema.json", + "identifier": "run-app-base", + "description": "Base permissions to run the app", + "windows": ["main"], + "permissions": [ + "log:default", + { + "identifier": "http:default", + "allow": [ + "https://tauri.app", + { + "url": "http://localhost:3003" + } + ] + }, + "core:default", + "core:app:allow-set-app-theme", + "core:window:allow-minimize", + "core:window:allow-toggle-maximize", + "core:window:allow-set-size", + "core:window:allow-close", + "core:webview:allow-create-webview-window", + "core:window:allow-start-dragging", + "notification:default", + "os:default", + "os:allow-hostname", + "dialog:default", + { + "identifier": "shell:allow-spawn", + "allow": [ + { + "name": "sh", + "cmd": "sh", + "args": [ + "-c", + { + "validator": ".+" + } + ] + }, + { + "name": "cmd", + "cmd": "cmd", + "args": [ + "/C", + { + "validator": ".+" + } + ] + } + ] + }, + { + "identifier": "shell:allow-execute", + "allow": [ + { + "name": "sh", + "cmd": "sh", + "args": [ + "-c", + { + "validator": ".+" + } + ] + }, + { + "name": "cmd", + "cmd": "cmd", + "args": [ + "/C", + { + "validator": ".+" + } + ] + } + ] + }, + "shell:default", + "shell:allow-kill", + "shell:allow-stdin-write", + "process:allow-exit", + "process:allow-restart", + "clipboard-manager:allow-read-text", + "clipboard-manager:allow-write-text", + "clipboard-manager:allow-read-image", + "clipboard-manager:allow-write-image", + "clipboard-manager:allow-write-html", + "clipboard-manager:allow-clear", + "fs:default", + "fs:read-meta", + "fs:allow-open", + "fs:allow-write", + "fs:allow-read", + "fs:allow-rename", + "fs:allow-mkdir", + "fs:allow-remove", + "fs:allow-write-text-file", + "fs:allow-write-file", + "fs:allow-create", + "fs:allow-copy-file", + "fs:allow-seek", + "fs:allow-truncate", + "fs:allow-ftruncate", + "fs:allow-unwatch", + "fs:scope-download-recursive", + "fs:scope-resource-recursive", + { + "identifier": "fs:scope-appdata-recursive", + "allow": [ + { + "path": "$APPDATA/db/" + }, + { + "path": "$APPDATA/db/**" + } + ], + "deny": ["$APPDATA/db/*.stronghold"] + }, + "store:default", + "opener:default", + { + "identifier": "opener:allow-open-url", + "allow": [ + { + "url": "https://*", + "app": "inAppBrowser" + } + ] + }, + { + "identifier": "opener:allow-open-path", + "allow": [{ "path": "$APPDATA" }, { "path": "$APPDATA/**" }] + }, + "upload:default", + "deep-link:default", + "deep-link:allow-register", + "deep-link:allow-unregister", + "deep-link:allow-is-registered", + "sql:default", + "sql:allow-execute", + "stronghold:default", + "stronghold:allow-destroy", + "stronghold:allow-remove-secret", + "stronghold:allow-remove-store-record", + "websocket:default" + ] +} diff --git a/examples/api/src-tauri/capabilities/desktop.json b/examples/api/src-tauri/capabilities/desktop.json new file mode 100644 index 0000000..26722c2 --- /dev/null +++ b/examples/api/src-tauri/capabilities/desktop.json @@ -0,0 +1,19 @@ +{ + "$schema": "../gen/schemas/desktop-schema.json", + "identifier": "run-app-desktop", + "description": "Permissions to run the app (desktop only)", + "windows": ["main"], + "platforms": ["linux", "macOS", "windows"], + "permissions": [ + "autostart:default", + "cli:default", + "positioner:default", + "updater:default", + "global-shortcut:allow-unregister", + "global-shortcut:allow-register", + "global-shortcut:allow-unregister-all", + "global-shortcut:allow-is-registered", + "window-state:default", + { "identifier": "fs:allow-watch", "allow": ["*", "**/*"] } + ] +} diff --git a/examples/api/src-tauri/capabilities/mobile.json b/examples/api/src-tauri/capabilities/mobile.json new file mode 100644 index 0000000..da77f5e --- /dev/null +++ b/examples/api/src-tauri/capabilities/mobile.json @@ -0,0 +1,24 @@ +{ + "$schema": "../gen/schemas/mobile-schema.json", + "identifier": "run-app-mobile", + "description": "Permissions to run the app (mobile only)", + "windows": ["main"], + "platforms": ["android", "iOS"], + "permissions": [ + "nfc:allow-write", + "nfc:allow-scan", + "biometric:allow-authenticate", + "barcode-scanner:allow-scan", + "barcode-scanner:allow-cancel", + "barcode-scanner:allow-request-permissions", + "barcode-scanner:allow-check-permissions", + "geolocation:allow-check-permissions", + "geolocation:allow-request-permissions", + "geolocation:allow-watch-position", + "geolocation:allow-get-current-position", + "haptics:allow-impact-feedback", + "haptics:allow-notification-feedback", + "haptics:allow-selection-feedback", + "haptics:allow-vibrate" + ] +} diff --git a/examples/api/src-tauri/gen/android/.editorconfig b/examples/api/src-tauri/gen/android/.editorconfig new file mode 100644 index 0000000..ebe51d3 --- /dev/null +++ b/examples/api/src-tauri/gen/android/.editorconfig @@ -0,0 +1,12 @@ +# EditorConfig is awesome: https://EditorConfig.org + +# top-most EditorConfig file +root = true + +[*] +indent_style = space +indent_size = 2 +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = false +insert_final_newline = false \ No newline at end of file diff --git a/examples/api/src-tauri/gen/android/.gitignore b/examples/api/src-tauri/gen/android/.gitignore new file mode 100644 index 0000000..b248203 --- /dev/null +++ b/examples/api/src-tauri/gen/android/.gitignore @@ -0,0 +1,19 @@ +*.iml +.gradle +/local.properties +/.idea/caches +/.idea/libraries +/.idea/modules.xml +/.idea/workspace.xml +/.idea/navEditor.xml +/.idea/assetWizardSettings.xml +.DS_Store +build +/captures +.externalNativeBuild +.cxx +local.properties +key.properties + +/.tauri +/tauri.settings.gradle \ No newline at end of file diff --git a/examples/api/src-tauri/gen/android/app/.gitignore b/examples/api/src-tauri/gen/android/app/.gitignore new file mode 100644 index 0000000..4008dd7 --- /dev/null +++ b/examples/api/src-tauri/gen/android/app/.gitignore @@ -0,0 +1,6 @@ +/src/main/java/com/tauri/api/generated +/src/main/jniLibs/**/*.so +/src/main/assets/tauri.conf.json +/tauri.build.gradle.kts +/proguard-tauri.pro +/tauri.properties \ No newline at end of file diff --git a/examples/api/src-tauri/gen/android/app/build.gradle.kts b/examples/api/src-tauri/gen/android/app/build.gradle.kts new file mode 100644 index 0000000..029d0f4 --- /dev/null +++ b/examples/api/src-tauri/gen/android/app/build.gradle.kts @@ -0,0 +1,69 @@ +import java.util.Properties + +plugins { + id("com.android.application") + id("org.jetbrains.kotlin.android") + id("rust") +} + +val tauriProperties = Properties().apply { + val propFile = file("tauri.properties") + if (propFile.exists()) { + propFile.inputStream().use { load(it) } + } +} + +android { + compileSdk = 36 + namespace = "com.tauri.api" + defaultConfig { + manifestPlaceholders["usesCleartextTraffic"] = "false" + applicationId = "com.tauri.api" + minSdk = 24 + targetSdk = 36 + versionCode = tauriProperties.getProperty("tauri.android.versionCode", "1").toInt() + versionName = tauriProperties.getProperty("tauri.android.versionName", "1.0") + } + buildTypes { + getByName("debug") { + manifestPlaceholders["usesCleartextTraffic"] = "true" + isDebuggable = true + isJniDebuggable = true + isMinifyEnabled = false + packaging { jniLibs.keepDebugSymbols.add("*/arm64-v8a/*.so") + jniLibs.keepDebugSymbols.add("*/armeabi-v7a/*.so") + jniLibs.keepDebugSymbols.add("*/x86/*.so") + jniLibs.keepDebugSymbols.add("*/x86_64/*.so") + } + } + getByName("release") { + isMinifyEnabled = true + proguardFiles( + *fileTree(".") { include("**/*.pro") } + .plus(getDefaultProguardFile("proguard-android-optimize.txt")) + .toList().toTypedArray() + ) + } + } + kotlinOptions { + jvmTarget = "1.8" + } + buildFeatures { + buildConfig = true + } +} + +rust { + rootDirRel = "../../../" +} + +dependencies { + implementation("androidx.webkit:webkit:1.6.1") + implementation("androidx.appcompat:appcompat:1.6.1") + implementation("com.google.android.material:material:1.8.0") + testImplementation("junit:junit:4.13.2") + androidTestImplementation("androidx.test.ext:junit:1.1.4") + androidTestImplementation("androidx.test.espresso:espresso-core:3.5.0") +} + +apply(from = "tauri.build.gradle.kts") \ No newline at end of file diff --git a/examples/api/src-tauri/gen/android/app/proguard-rules.pro b/examples/api/src-tauri/gen/android/app/proguard-rules.pro new file mode 100644 index 0000000..481bb43 --- /dev/null +++ b/examples/api/src-tauri/gen/android/app/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/examples/api/src-tauri/gen/android/app/src/main/AndroidManifest.xml b/examples/api/src-tauri/gen/android/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..8ebe7c4 --- /dev/null +++ b/examples/api/src-tauri/gen/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,57 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/examples/api/src-tauri/gen/android/app/src/main/java/com/tauri/api/MainActivity.kt b/examples/api/src-tauri/gen/android/app/src/main/java/com/tauri/api/MainActivity.kt new file mode 100644 index 0000000..e89f632 --- /dev/null +++ b/examples/api/src-tauri/gen/android/app/src/main/java/com/tauri/api/MainActivity.kt @@ -0,0 +1,11 @@ +package com.tauri.api + +import android.os.Bundle +import androidx.activity.enableEdgeToEdge + +class MainActivity : TauriActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + enableEdgeToEdge() + super.onCreate(savedInstanceState) + } +} diff --git a/examples/api/src-tauri/gen/android/app/src/main/res/drawable-v24/ic_launcher_foreground.xml b/examples/api/src-tauri/gen/android/app/src/main/res/drawable-v24/ic_launcher_foreground.xml new file mode 100644 index 0000000..2b068d1 --- /dev/null +++ b/examples/api/src-tauri/gen/android/app/src/main/res/drawable-v24/ic_launcher_foreground.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/examples/api/src-tauri/gen/android/app/src/main/res/drawable/ic_launcher_background.xml b/examples/api/src-tauri/gen/android/app/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 0000000..07d5da9 --- /dev/null +++ b/examples/api/src-tauri/gen/android/app/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/examples/api/src-tauri/gen/android/app/src/main/res/layout/activity_main.xml b/examples/api/src-tauri/gen/android/app/src/main/res/layout/activity_main.xml new file mode 100644 index 0000000..4fc2444 --- /dev/null +++ b/examples/api/src-tauri/gen/android/app/src/main/res/layout/activity_main.xml @@ -0,0 +1,18 @@ + + + + + + \ No newline at end of file diff --git a/examples/api/src-tauri/gen/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/examples/api/src-tauri/gen/android/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..28f1aa119119336781390c2531c381499576ed59 GIT binary patch literal 3524 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 literal 0 HcmV?d00001 diff --git a/examples/api/src-tauri/gen/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png b/examples/api/src-tauri/gen/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png new file mode 100644 index 0000000000000000000000000000000000000000..28f1aa119119336781390c2531c381499576ed59 GIT binary patch literal 3524 zcmV;#4LkCQP)qWSpyDKsw z@SLEk+Ip+_KCZK+%Yb|B4_LOb-_x9os ziz^f-{8_*&)mxz0ZG+%6?sFnlnbNn1wdb0-omM9&+XoN|F}PKNz#t%%_y@&l5ID%3 z$eZC;t3ayqJw@|A%AdP(N6BU*t32FOz2hJ_eL)2XlFl3*KH-Bo9XAn?BZ`wM_&11} zAwb>)S4q5{0feG3Eendm6hxx}M9IucBv2qbca?4${o2nHDJ(*BfpE8316l?aJrnWI zS7${nBNYn4_Xthkfff)H@iKDrrswiycvU%Rh1tYeI=3@*>K0{*IE`l6dTJW%_`bmmZM;EYOSBGI=LJ_-bJ4sIYM>7vA%!QYNba3PGr%O%RF9l+E9k zu3Gzb$@0&c7aAWiV^zW39g44=e~_eK!-(iVmtN>LKJ26Xi`&L$VN zB{YIUq@xE051IXs$OV{aF+dP`Xm3YPRPTOh)}@h{dkLunzowuXitwt8Z2CCv^Be?5EyX zW2!d;Y9p&JdwG1jX{}lt+JkwaE-=v~azPIa7!c4izNZgILje5~Y>D45iP?T>OWe84 zkGdb8_C)x^#;D+{wXBbO{D?rj=}8`Tq7O@77PHf+i$>`$-Xa1HjRR3BHHC#ag3_LT z-uCG5iI!gX#^*hgHi(e3Iq)Q&P)ZR1P>K%i8+8Bf<=u`D3BvUN69kT=-0$euy^}e1 z<_CH6foK8{kNRi_G*fr~RP|j_N??D6r)8D&>}=@;Dki$1pI`4bQi)mO%)DOIB1Ed-6%Qrb2l>;8 z8S9&0qLg5#_st^knE;SudsAX0HY)=_AI@yyqdxjP!`ipsK&7NuhPEYWb!-VBydyZo zPe(aF9c{ePGN18koneUo-6~e~jvu+~>U52POsAet5<0e(+7c62cOGo8 z3w)H*pK{Fx!SMoSiiJ*{JJ0^`un~{}FHlOt4Df+LT=~Y;@8pge)V1QXjq8oIWeu&# zjQ8`V0k0Kv8_@!`n@3;8hf8GXrh)~T7hE~|AXTUR8jQr~fMhHP&G>;UhB>3KupB7X z7=quV6VQl34Vq))y0>{|`dhTD1Y4`9VGFR5!q;D%Q2E8DpK4Er^Gs3dO=PG{7+^+L zb!P8L=XGpVyO*A7H|4jQo!E3StJD(|46`SOv1li;Pll zoupu3P%8{27P+i7M9^bZ^+_mN5sC^0mK!HNe{0U+5QN9So{xCj&;*JGkygm=?FTA$ z>gwJ2fW$Xm&%ActK6vPpAf*BXE9*fkq;%Di`E`BI2S6J(;RJ;X5P&8KQ0WK=GKw_8 zVTnW22OZ2G0Lrl;yY#&&9p?4!9XJRkL#j12yFj+>uh?=W-;*z(@od5XHkt`|q!yjE zPrpZ*ENcqR+ib-3)8X{mGnr>FFbJ5|)uIlDIYy5gA2edjb8u~lH}}ffvQJhl^5kUa zXw+1VoBAEN(x5uflbO0;Qv0cMCr7+RD=8Cr&4WqANxiRcC|IQTkfz5}u`z3bEPxCI zNL{+dF|S8IEt!bx*uig%OmTIKf}97MgwKBE&CsbY&Zo|5%mOjTB_@EPjmdj{?0xQ& z$1?SXN$V9Q0&v&_^NJUu7LOklK3%P*P(v`$DhzCm#J{%m#@zGQYp?&_Ru11N2rS5u z&~PEp5e_SkK?|4>&xLcR4Zbq}+OZ_0B!>Kb+I+e27shIw3iE)Jl$F$Sq9laRS+GL4 zBUG(lc7)8E&~9e#=kdAYp9r5$s}-Crm{%W9rEhnaZFys3!EzLU-`k67dRhu{*r7|C zaVtWTTL3}JOG}86TmRY#=#x5_=yj|RY=oF9$z)btHN?~XcXgbPthd*(e^o3ka0&RCMm~ocR$ML@qqD^# z<|R&$LM3tQ_TtqKOuIP3H(WJm6Rk-Qw5rm{JhdJz?`Zxci3n{%!~7|y84NhwS;sT9 zjB%jjhC(sf>*1|7;?2E$8eDFkwy1`~)v{;iZGl!Hl$d#93YAP{W%BjO^RJG6YEkYK zy}yuVZ=Ugutel1%s8d9%tBOJUF(!&2%-!RY43Edv*=|st+EXXbl&@d;meekS<7{@3 zGn;v#O_&9M(BdVFrWsC{*K0wyBdPJ$6l*(^Bjfuq)ZwkP&XK8rqd&J%n3CW~6PZJ&QPZ|IC2Y2(Lr+_2+^&R^_19kF=L7ITkY zJsV!n8#QrnH(U2Xy@OMU->pRc);BJi#vo`OiL`lc)|~D+#Q-A58*}s`>y~w$SrStc zjP5MdbG!cbgXe!`U+RzH^$&Bf!x5Y|?yo{{h`r&6+hQy3;4PX%iSKDj@SeF3Z0TZmPj+?(e7<)R8;eC;=m~Ct7gZ*@E z7jBtS!f$N~;EKrCu4Fq#jhzlFs{NM=6pOPxICz*+Qh2pC`*fBOpyV$K_3i;dX~qz& zI(GfY<(s~}qy*ta+e(;{pp0pkajUJ+?6c-l5>dE#&Lmvh= zt8m!(AfCWShd+VV#C`H;1~fui2LJ=nn;>UwEHG3tOG0vdhvl302BiF@KdmfRkTsmi zYyt&y=f5y{m|9t0?pkDPbyyj*be1_1Pc z_X?3BJH|+nQJo0JS5>Kv7x*t!tPhw?Na2Dx&sJ{R{E0?4Y?3Wi)8-Yq?m$tU+P|}M zbI|a#@oIG?Q9T~?JUCcrvJ6cQ>O;~c7Zw%$7Pw$^(TXwGap1`W)$Q^YEqZBe)voPd z5uD@ImIG)8o&37EzIDT#dw14VtCe>hIj}o__T6T0G(QvdZ93{W6@uH?gU3ES#s1jHNv3;Z z;&|9w)=I(8?g00-K~Jr$aJf$%Plt_#<<5*f-}@RWZbY<7b8cxz_XU8vf|g!MDKJx{ zg6)ojVr>hNOHeOPrRbOI-itZ^b)PzU6zV0v&eG`nSt|L%4=e`5o;0_4axG|NcPtYP y!07X;)*G~$kC8g7tM8|2^YLA=@_!z8_V^!OU)8o@2#nAG0000QSgd_^dWEm#2zvb6^-@HdA8zdoW|L`5&dC6Pv^4I#TRMNHHdIk^#JBgGvgR#^Dg%|e zBXA(y%?RB)QKuPMA?kpX>e!fD3~^kHdV{Ru1nai_yyZkEMu<(^H8 zR@c4yS@HQTcq_-!oF0Uq#R2_;JV#Ui`fJ~Xvu{h7PZ=1B^2(GX5ee)Ds8dA$5Yavg z8DroXo-(uQiLbUat^3obZ;1+nkd*eF3R{~(2%GR#3fOX4KWN0 znq!PmwgRnM9QFWQ5Md7}6g(^HrNG%Z*6S3VH_SgZ8M0E^)lATr0UH(!mHQ_N@uhDM zDzPUS287&Fh{J*;V7nD?A`$BNNq>3iabIb5U8oxIofOA8hNYWwhi92bQPC__SK7s@`=LSOGbMIBmKolM(&7z{bFv0DdXg-!TU)` z0TI8+6>%qf^U0y*IlC^zt2fz-)yQc=o+Y2`8csrA=SpP9a6(E8h;QSSPB3VoRv)~6 zZzuhq5ja}FpG^jaDSt0}7aA^Y$(7)pAq>p?yAuDb*IMr}Y`9-j#Y?g_l4GL?M&0@8 z(3*oqIlCw1TJrIlz=sWs#3`c^9EBX*DxPX5f-NbIk3PR)*pH-mI65KX5#D>oYGwo- zw!LAzn)=aKL=CO`Wa!Do5A|Jeb%AYkY>Z%So1sXKXj^M-?-4X7W=x8^iR>#-v!Ft$ ztbokC-hO+pWkHWZYLX!_KF$!+>KcPY+=A`4hF+Or$}Y$@4}-#IHOaxYdAIs3V1EzU zosZBAqB9#_PG8-T(-(r;9S5-UFdWNOoeS31HwMoeVyscN8*iDG_{0mZFxD6ol9pa_ zdiMZk&OG!%&aU#JoFgUgX78_9J@~}U<7|_#bw(PcWr|w0xoIU5q1w2D3Y8D-7GZ?% zx@;KHEUTuGjfxWDADsKxz%TYxW>W5#JXklb@C5p!c8)tQWKCA`{T;7tWz%G+r z`2>sgw^v&7A|tCiFlI928U{D!`C9*7;e=~k;>e|gm z><`X=k&TV(uXvg%f9dY6VHh!t#=xpk_f4_SjOlAg2{uXrxI&`=A+H=nEj0H|$Gk`` z0>!Nc9fV$uzgcoI97U`8%D`|5F}x#uWBZ2MRR>r~QRLWr?wi&3tq*r_iOFeFy;CHi zU;x9S_`KeZco7(PFv*o|S81mzE zNwpop0^TT?jf??J*D(*rP>&oy;}JHNt0(;~Vcvozazj0Vx8ZDsJLoNB(xipeybz_s``6P>#jUzHa z(qJKjq^Vi14~M6{C}A!F8yXD)oJTvJT)2YxeX4C3LNK0nf41)ZGt;ik@oZYZo&@wD#GLJj*Bdbw~~MB07bUKfmrj|E_)dkq5#;hvSJP ziEW=pY$I#0+2Y!`wcIms(#YtECV??-Kj0}o3Xb8U3Tc_X^di2iFm^iDjXM57J3VmP z|6OCV?8SE}5m3AY@Wb7NPf6{QvGbTOW6w#FQG8U)Dz72^R(!f6f|{8&R1I;5GR3;l z`JjV&w=F4woKOQIBM*jBxTerzJa#IGJIrBN3u>XTEJ6hFjdKDTfkhiBkMTUzhlF2T@^HVUm=lP< z%=0wwcZOUmH#M!jQ2ZSYRUI;}BCCc)jhQf&ghFb98H<(jeEr2ofdggVQ)+QbQY*a% zol3|Ulx`Z9f0Ysl_*)Zl%q_8ZMyM4(VP#p4!(hY#%23Qo}+f$@BlrZ<{mq;K$$xV$C>>^~$*5`#cJ`)C{ zr&5Y6>-9owYyrJqK&1)8%<;nUwrP?}LT!X!XDObhW;}Q1ZkI<6BE&lCK9_WWwI{^W zynqWeGB`lVuBLjazOGJh84(96=H@$R6Q2+7713GjOp4bVWGogQvNl6Y6cahkEqWm3 z*>@LbtajM>0W!b_z6u-z-PFcWb6%?Y!|Jwj{a5K`3&Xe38g$r6Y}5aEzs9~T(9=3h)cC@}fuHX@k-4ve zWR#qi`xSk>O-Rce5Sb~xo7kf{>(alcK7ac}`;8{EU}#aQOfL=ZOnB@|dGmu$vc5ly z4g@nJS(t9auG;;yV4w}zLk0`!S?L#J>$aIU_DM<*Qu-yf&Tbb??Ia26OFUaQuA|jp z(+u%>6jR>Fv6gFZxE^38(>%{d6tj%13y>qhqX!R%P8~m`@&g*%imh2z`&|+2_PFRV zG=e78$%Q6BB6K_c`2uwmv|!`lVOJ!-QM3g_Vk#JGA)$Z* z-=NMtIcJ*etaYmGnhEU!Lq<6#i@?@X`@XYwFJm-YWVZ{((Lj*1I~dp66$^pk4{MWOxBuL9 zalEORm1RRlc6z^+>krZlYa}8JFAyU-Dhe_b5M8U6zv}*E_1mG-Co0;#9Q-imjT{v_ zot0^{TC0^EPN zX6CJ<ETEiFX%?bP zArV5BaM+Uc`(VDHe0#p#`r||QPLa>MyeJ3(Vm)3y?{dr1o!}Dt*d32Qzsxdm@@;Ad z^?E2gkFAwzCmPkR1Y49LXh2pie6c2Wjn2^Z30^9T)lVurZr6;y!9$@M8yR{>mh7<>_7yQsuLLg3VOf4 zso*yg3^_wb@O@GSKwFJbXL`S{L9D7c7Wlqwr*Ge`?J%9WFVqxvxE>U2?`}BEXcD5U z+3Sele*O$}kdg4PLc&WUdSp?LgeyAy9}0f)i2u6%G~53G-3(h6(Dps_00000NkvXX Hu0mjfi}+A- literal 0 HcmV?d00001 diff --git a/examples/api/src-tauri/gen/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png b/examples/api/src-tauri/gen/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png new file mode 100644 index 0000000000000000000000000000000000000000..13dd21476b5c7f69cdfb82969bce7150244c127f GIT binary patch literal 9081 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 literal 0 HcmV?d00001 diff --git a/examples/api/src-tauri/gen/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png b/examples/api/src-tauri/gen/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png new file mode 100644 index 0000000000000000000000000000000000000000..73e48dbfb7dc31ed1e951b77820af88b93fcc418 GIT binary patch literal 3377 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 Hu0mjfi}+A- literal 0 HcmV?d00001 diff --git a/examples/api/src-tauri/gen/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/examples/api/src-tauri/gen/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..1d98044f13bdd3afc7cb426644ae118688c03c14 GIT binary patch literal 7971 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 Z{{b+Ide7&7`HKJm002ovPDHLkV1iT#V`TsU literal 0 HcmV?d00001 diff --git a/examples/api/src-tauri/gen/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png b/examples/api/src-tauri/gen/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png new file mode 100644 index 0000000000000000000000000000000000000000..a888b336b5bc84b42c0c266ce9b8ec5d9eafed0b GIT binary patch literal 18900 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 I*#%+#2dtK;n*aa+ literal 0 HcmV?d00001 diff --git a/examples/api/src-tauri/gen/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/examples/api/src-tauri/gen/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png new file mode 100644 index 0000000000000000000000000000000000000000..1d98044f13bdd3afc7cb426644ae118688c03c14 GIT binary patch literal 7971 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 Z{{b+Ide7&7`HKJm002ovPDHLkV1iT#V`TsU literal 0 HcmV?d00001 diff --git a/examples/api/src-tauri/gen/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/examples/api/src-tauri/gen/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..081832466b4a17180fe9255fa0d3fcba2010592c GIT binary patch literal 12392 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 K@hVZnfd2y&>ZwQo literal 0 HcmV?d00001 diff --git a/examples/api/src-tauri/gen/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png b/examples/api/src-tauri/gen/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png new file mode 100644 index 0000000000000000000000000000000000000000..a2a838e7b550ac82b12e73d71427c6ffea06a944 GIT binary patch literal 29506 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 K@hVZnfd2y&>ZwQo literal 0 HcmV?d00001 diff --git a/examples/api/src-tauri/gen/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/examples/api/src-tauri/gen/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..b18bceb64d257ed31d50e65d7f2b98abcfb21158 GIT binary patch literal 16751 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 XrJNsHnTG=pQeg0O^>bP0l+XkK^)z{o literal 0 HcmV?d00001 diff --git a/examples/api/src-tauri/gen/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png b/examples/api/src-tauri/gen/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png new file mode 100644 index 0000000000000000000000000000000000000000..3f8a57f38e0273699c8cdd1012042bffdc38abce GIT binary patch literal 40510 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

C z(CX6fIh8mroxe4Fy?l{bj%8R307X*fDQ#(v@y+x%4>XY?ax zC^p*eaCfrcq#aa-hT`A*7=V1wKdlx2BoX`y?{jb4s1j@)b7-etD6E@cKXx@8e&Vf zWehdcr8jm&_uNAX*$0NY%B&gn)>)@jQLcO5F3UZEr zds`B64=#T7qE6myk{5rNg+99@E{Zf&GEI$b?&PagH-fv#5k*V6EEmp!A*tE}RWk_SsM5#wN>{l7X5DBVl)bv~bNp*#NXY*I)h$Ng literal 0 HcmV?d00001 diff --git a/examples/api/src-tauri/gen/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/examples/api/src-tauri/gen/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png new file mode 100644 index 0000000000000000000000000000000000000000..b18bceb64d257ed31d50e65d7f2b98abcfb21158 GIT binary patch literal 16751 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 XrJNsHnTG=pQeg0O^>bP0l+XkK^)z{o literal 0 HcmV?d00001 diff --git a/examples/api/src-tauri/gen/android/app/src/main/res/values-night/themes.xml b/examples/api/src-tauri/gen/android/app/src/main/res/values-night/themes.xml new file mode 100644 index 0000000..4042aae --- /dev/null +++ b/examples/api/src-tauri/gen/android/app/src/main/res/values-night/themes.xml @@ -0,0 +1,6 @@ + + + + diff --git a/examples/api/src-tauri/gen/android/app/src/main/res/values/colors.xml b/examples/api/src-tauri/gen/android/app/src/main/res/values/colors.xml new file mode 100644 index 0000000..f8c6127 --- /dev/null +++ b/examples/api/src-tauri/gen/android/app/src/main/res/values/colors.xml @@ -0,0 +1,10 @@ + + + #FFBB86FC + #FF6200EE + #FF3700B3 + #FF03DAC5 + #FF018786 + #FF000000 + #FFFFFFFF + \ No newline at end of file diff --git a/examples/api/src-tauri/gen/android/app/src/main/res/values/strings.xml b/examples/api/src-tauri/gen/android/app/src/main/res/values/strings.xml new file mode 100644 index 0000000..6f4305e --- /dev/null +++ b/examples/api/src-tauri/gen/android/app/src/main/res/values/strings.xml @@ -0,0 +1,4 @@ + + Tauri API + Tauri API + \ No newline at end of file diff --git a/examples/api/src-tauri/gen/android/app/src/main/res/values/themes.xml b/examples/api/src-tauri/gen/android/app/src/main/res/values/themes.xml new file mode 100644 index 0000000..4042aae --- /dev/null +++ b/examples/api/src-tauri/gen/android/app/src/main/res/values/themes.xml @@ -0,0 +1,6 @@ + + + + diff --git a/examples/api/src-tauri/gen/android/app/src/main/res/xml/file_paths.xml b/examples/api/src-tauri/gen/android/app/src/main/res/xml/file_paths.xml new file mode 100644 index 0000000..782d63b --- /dev/null +++ b/examples/api/src-tauri/gen/android/app/src/main/res/xml/file_paths.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/examples/api/src-tauri/gen/android/build.gradle.kts b/examples/api/src-tauri/gen/android/build.gradle.kts new file mode 100644 index 0000000..607240b --- /dev/null +++ b/examples/api/src-tauri/gen/android/build.gradle.kts @@ -0,0 +1,22 @@ +buildscript { + repositories { + google() + mavenCentral() + } + dependencies { + classpath("com.android.tools.build:gradle:8.11.0") + classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.9.25") + } +} + +allprojects { + repositories { + google() + mavenCentral() + } +} + +tasks.register("clean").configure { + delete("build") +} + diff --git a/examples/api/src-tauri/gen/android/buildSrc/build.gradle.kts b/examples/api/src-tauri/gen/android/buildSrc/build.gradle.kts new file mode 100644 index 0000000..5c55bba --- /dev/null +++ b/examples/api/src-tauri/gen/android/buildSrc/build.gradle.kts @@ -0,0 +1,23 @@ +plugins { + `kotlin-dsl` +} + +gradlePlugin { + plugins { + create("pluginsForCoolKids") { + id = "rust" + implementationClass = "RustPlugin" + } + } +} + +repositories { + google() + mavenCentral() +} + +dependencies { + compileOnly(gradleApi()) + implementation("com.android.tools.build:gradle:8.11.0") +} + diff --git a/examples/api/src-tauri/gen/android/buildSrc/src/main/java/com/tauri/api/kotlin/BuildTask.kt b/examples/api/src-tauri/gen/android/buildSrc/src/main/java/com/tauri/api/kotlin/BuildTask.kt new file mode 100644 index 0000000..f764e2a --- /dev/null +++ b/examples/api/src-tauri/gen/android/buildSrc/src/main/java/com/tauri/api/kotlin/BuildTask.kt @@ -0,0 +1,68 @@ +import java.io.File +import org.apache.tools.ant.taskdefs.condition.Os +import org.gradle.api.DefaultTask +import org.gradle.api.GradleException +import org.gradle.api.logging.LogLevel +import org.gradle.api.tasks.Input +import org.gradle.api.tasks.TaskAction + +open class BuildTask : DefaultTask() { + @Input + var rootDirRel: String? = null + @Input + var target: String? = null + @Input + var release: Boolean? = null + + @TaskAction + fun assemble() { + val executable = """pnpm"""; + try { + runTauriCli(executable) + } catch (e: Exception) { + if (Os.isFamily(Os.FAMILY_WINDOWS)) { + // Try different Windows-specific extensions + val fallbacks = listOf( + "$executable.exe", + "$executable.cmd", + "$executable.bat", + ) + + var lastException: Exception = e + for (fallback in fallbacks) { + try { + runTauriCli(fallback) + return + } catch (fallbackException: Exception) { + lastException = fallbackException + } + } + throw lastException + } else { + throw e; + } + } + } + + fun runTauriCli(executable: String) { + val rootDirRel = rootDirRel ?: throw GradleException("rootDirRel cannot be null") + val target = target ?: throw GradleException("target cannot be null") + val release = release ?: throw GradleException("release cannot be null") + val args = listOf("tauri", "android", "android-studio-script"); + + project.exec { + workingDir(File(project.projectDir, rootDirRel)) + executable(executable) + args(args) + if (project.logger.isEnabled(LogLevel.DEBUG)) { + args("-vv") + } else if (project.logger.isEnabled(LogLevel.INFO)) { + args("-v") + } + if (release) { + args("--release") + } + args(listOf("--target", target)) + }.assertNormalExitValue() + } +} \ No newline at end of file diff --git a/examples/api/src-tauri/gen/android/buildSrc/src/main/java/com/tauri/api/kotlin/RustPlugin.kt b/examples/api/src-tauri/gen/android/buildSrc/src/main/java/com/tauri/api/kotlin/RustPlugin.kt new file mode 100644 index 0000000..4aa7fca --- /dev/null +++ b/examples/api/src-tauri/gen/android/buildSrc/src/main/java/com/tauri/api/kotlin/RustPlugin.kt @@ -0,0 +1,85 @@ +import com.android.build.api.dsl.ApplicationExtension +import org.gradle.api.DefaultTask +import org.gradle.api.Plugin +import org.gradle.api.Project +import org.gradle.kotlin.dsl.configure +import org.gradle.kotlin.dsl.get + +const val TASK_GROUP = "rust" + +open class Config { + lateinit var rootDirRel: String +} + +open class RustPlugin : Plugin { + private lateinit var config: Config + + override fun apply(project: Project) = with(project) { + config = extensions.create("rust", Config::class.java) + + val defaultAbiList = listOf("arm64-v8a", "armeabi-v7a", "x86", "x86_64"); + val abiList = (findProperty("abiList") as? String)?.split(',') ?: defaultAbiList + + val defaultArchList = listOf("arm64", "arm", "x86", "x86_64"); + val archList = (findProperty("archList") as? String)?.split(',') ?: defaultArchList + + val targetsList = (findProperty("targetList") as? String)?.split(',') ?: listOf("aarch64", "armv7", "i686", "x86_64") + + extensions.configure { + @Suppress("UnstableApiUsage") + flavorDimensions.add("abi") + productFlavors { + create("universal") { + dimension = "abi" + ndk { + abiFilters += abiList + } + } + defaultArchList.forEachIndexed { index, arch -> + create(arch) { + dimension = "abi" + ndk { + abiFilters.add(defaultAbiList[index]) + } + } + } + } + } + + afterEvaluate { + for (profile in listOf("debug", "release")) { + val profileCapitalized = profile.replaceFirstChar { it.uppercase() } + val buildTask = tasks.maybeCreate( + "rustBuildUniversal$profileCapitalized", + DefaultTask::class.java + ).apply { + group = TASK_GROUP + description = "Build dynamic library in $profile mode for all targets" + } + + tasks["mergeUniversal${profileCapitalized}JniLibFolders"].dependsOn(buildTask) + + for (targetPair in targetsList.withIndex()) { + val targetName = targetPair.value + val targetArch = archList[targetPair.index] + val targetArchCapitalized = targetArch.replaceFirstChar { it.uppercase() } + val targetBuildTask = project.tasks.maybeCreate( + "rustBuild$targetArchCapitalized$profileCapitalized", + BuildTask::class.java + ).apply { + group = TASK_GROUP + description = "Build dynamic library in $profile mode for $targetArch" + rootDirRel = config.rootDirRel + target = targetName + release = profile == "release" + } + + buildTask.dependsOn(targetBuildTask) + tasks["merge$targetArchCapitalized${profileCapitalized}JniLibFolders"].dependsOn( + targetBuildTask + ) + } + } + } + } +} \ No newline at end of file diff --git a/examples/api/src-tauri/gen/android/gradle.properties b/examples/api/src-tauri/gen/android/gradle.properties new file mode 100644 index 0000000..2a7ec69 --- /dev/null +++ b/examples/api/src-tauri/gen/android/gradle.properties @@ -0,0 +1,24 @@ +# Project-wide Gradle settings. +# IDE (e.g. Android Studio) users: +# Gradle settings configured through the IDE *will override* +# any settings specified in this file. +# For more details on how to configure your build environment visit +# http://www.gradle.org/docs/current/userguide/build_environment.html +# Specifies the JVM arguments used for the daemon process. +# The setting is particularly useful for tweaking memory settings. +org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 +# When configured, Gradle will run in incubating parallel mode. +# This option should only be used with decoupled projects. More details, visit +# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects +# org.gradle.parallel=true +# AndroidX package structure to make it clearer which packages are bundled with the +# Android operating system, and which are packaged with your app"s APK +# https://developer.android.com/topic/libraries/support-library/androidx-rn +android.useAndroidX=true +# Kotlin code style for this project: "official" or "obsolete": +kotlin.code.style=official +# Enables namespacing of each library's R class so that its R class includes only the +# resources declared in the library itself and none from the library's dependencies, +# thereby reducing the size of the R class for that library +android.nonTransitiveRClass=true +android.nonFinalResIds=false \ No newline at end of file diff --git a/examples/api/src-tauri/gen/android/gradle/wrapper/gradle-wrapper.jar b/examples/api/src-tauri/gen/android/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..e708b1c023ec8b20f512888fe07c5bd3ff77bb8f GIT binary patch literal 59203 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

K7UpUK$>l@ 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 E2O(~oYXATM literal 0 HcmV?d00001 diff --git a/examples/api/src-tauri/gen/android/gradle/wrapper/gradle-wrapper.properties b/examples/api/src-tauri/gen/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..c5f9a53 --- /dev/null +++ b/examples/api/src-tauri/gen/android/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Tue May 10 19:22:52 CST 2022 +distributionBase=GRADLE_USER_HOME +distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.3-bin.zip +distributionPath=wrapper/dists +zipStorePath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME diff --git a/examples/api/src-tauri/gen/android/gradlew b/examples/api/src-tauri/gen/android/gradlew new file mode 100755 index 0000000..4f906e0 --- /dev/null +++ b/examples/api/src-tauri/gen/android/gradlew @@ -0,0 +1,185 @@ +#!/usr/bin/env sh + +# +# Copyright 2015 the original author or authors. +# +# 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 +# +# https://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. +# + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin or MSYS, switch paths to Windows format before running java +if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=`expr $i + 1` + done + case $i in + 0) set -- ;; + 1) set -- "$args0" ;; + 2) set -- "$args0" "$args1" ;; + 3) set -- "$args0" "$args1" "$args2" ;; + 4) set -- "$args0" "$args1" "$args2" "$args3" ;; + 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=`save "$@"` + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +exec "$JAVACMD" "$@" diff --git a/examples/api/src-tauri/gen/android/gradlew.bat b/examples/api/src-tauri/gen/android/gradlew.bat new file mode 100644 index 0000000..107acd3 --- /dev/null +++ b/examples/api/src-tauri/gen/android/gradlew.bat @@ -0,0 +1,89 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/examples/api/src-tauri/gen/android/settings.gradle b/examples/api/src-tauri/gen/android/settings.gradle new file mode 100644 index 0000000..3939116 --- /dev/null +++ b/examples/api/src-tauri/gen/android/settings.gradle @@ -0,0 +1,3 @@ +include ':app' + +apply from: 'tauri.settings.gradle' diff --git a/examples/api/src-tauri/gen/apple/.gitignore b/examples/api/src-tauri/gen/apple/.gitignore new file mode 100644 index 0000000..6726e2f --- /dev/null +++ b/examples/api/src-tauri/gen/apple/.gitignore @@ -0,0 +1,3 @@ +xcuserdata/ +build/ +Externals/ diff --git a/examples/api/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-20x20@1x.png b/examples/api/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-20x20@1x.png new file mode 100644 index 0000000000000000000000000000000000000000..a6ac2a8ccf0aff8558f948fbc2c9befd5aa34361 GIT binary patch literal 1036 zcmZ{jdrVt(6vux^=qiX3s2gLFAdJBT+`Ui-HZI6Ug^odBfSZdL7%)L)4%X?Ib07hA zS>`ZBnHwl(Atsws7r|Mcqr4pBu@3Bm(Lu|r&5C7>4eS1K87N#!3gJRqnirZfRZ3AHJyk7{dUZAP~mklTt{3QZIC&moJ2BpI4 z)7T&diw1YU1Vb4vUHR;=0r6vgtwj2|TL9ZCk}tdQOj`ZqCG5=P*OVy-a+E z0NS`jJA(%&MRKhJpwnzwu+yNuilxtk^3%T6$4~btYG;k7ru5!2{I_ zixcC7vFV_}$r&4c+iWkLwkRLle>bdq_9*w=pLuAd?F>_{SwdaoZ~v|RN@sjVrrb85 zKBk(#pf!|h^fAL4_dAO0iCLNfsfpSz*KY1p`}8Vf?rARRj10iYy44Awro;N#GrJv4 zNlHXtdITo3#U`;RRk%sd@%MhpEpPC&iXDfD5)-og$Az`ohuleC^}9c&>t8++S=6pS zTBB3w%&cmHqs-6Z`huHOE!FsH?8z`UUg^26Oi6q$m&+fTp{lAHK^p;oE=WZVs2eNxl-5Q%8oxqy*O0HcGt6UDF)iYE4;Xps+u*IXq%9 zqH^f^dmsJKG@$+Y=Dto*5em5a!EnnpY4bPDEpJUG2n6j#TdU09zti+3`-tlv81*OI19yi`MyisoB?7scSXN{UY5$gE3tm}TY*YfMv zf0Ja!Cbl}%bbqMFCEN0qC`qVp!DHU6AAfG*btdI9 zw6tTtZ%Nz$oQM-luiB38gB1^n0uf)EhInd}DZ ze}Iw?3yP1G|3Bcx-M*g%!k=sq7nc;C6z7$kc)~>Uv&a|$5iKCdf0q=`nkbe*B4JwM JFMD&V{sG_UJ5T@s literal 0 HcmV?d00001 diff --git a/examples/api/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-20x20@2x-1.png b/examples/api/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-20x20@2x-1.png new file mode 100644 index 0000000000000000000000000000000000000000..2869541f735d2f3f890a5b400fcced58698cbac4 GIT binary patch literal 2216 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 R+5rQcJL_y$amF|Ce*nJiS(X3* literal 0 HcmV?d00001 diff --git a/examples/api/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-29x29@2x.png b/examples/api/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-29x29@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..a4e68c8d6d5c4a564be190fdd200d7214eda2db2 GIT binary patch literal 3394 zcmZ{nc{J3G_s2iR8e#@R@({|t4cW^UF)?Yf3}r${9%6`$X2Otas8O~?Wd_-jtQpIU zkz`Ok3MK21?XgQ3W6jU^{LcBF^ZVm>?z#7U&b{|_-v8d4jC6K5EPPZL0D!~i;C3zt zYW|-T0v*(_L3aFsDEh-)&I15W6#xiH0I+?~BFqB7Ef@gI`yT9O1At6;4*lZkgMomb zlY;F_CIzUv_GSx@Pun4gU*iw=#V4JLc%meWMDl z>KhHP%>FrkFxVO#BLBXO3^8%RvUouA0-RN~7nVHMHzrTKgN(p)+yJy^dhcg-X5ze= zz}qiB;QW;$AJ4-d=g*)5!(68NY%qKko=B0ODIKsa(X|Jca$hD9Kt_HBg4KZCEysDB z>^987vN)aBsd9hpr?4WSCd(vzBf^L)Ne9gNr%eKWG{xK}7q7Dgdb# z%4SV-I5sHy>itK?t%Gl#EvqFi%*$rbp4}k8g;IN_Ln&=1sQmtl;X-lIGeh^qv?pu+w2w~_) zam21G{id+D-Qw|_`WKePVYo*Vx}0tE$$V2_Ii8AoZ|DXJVz%YB%g%M!}v6H-Y$!PCuMYxn(XjtH0?=8YjnnrVsT4P(z5eOV-k7c9 z_V`+!P_YkPj}OldPH4>!s2$PJGD7r--GQ@r+lu&_s8SAmNsJic+?9dVa{*k(8Fe!G zNuQiy$3U?<8=NVqPoD{7Y-QkfH8z=G5q8n)gQ9H#*Hc3ZaqQy2GD$=zH18@?d19db zssD8~8=g-kwW_lhmI5g?3hw z*wfUeX9xy-RdO-9jRINsA8A4yH^2+6N&KU{kJU9X3Xh{ zY`H)zW}0~jnDX|`{ABlD79`X9x$5Sp>Bm4zZ|D;sdsebi&Tqn@!rt|=e&|v!4Rqep z#P!Z5LRU(3iweqxZB15fc8@j!n zq}3Fw6upGdV0*|;?_btX`Sr?YyT5+D=obm%PDG(oD9Mgik|o4ZRviX(wi{wOJe=1mM%v+kgGk{vU=;m znqkrX;hQhb+ZMd_mr@$URyxsO%P8ECha*IKtOuQN_i%~umw9Roe9m3 z>k>i9WuRc0DKoC?Y}9V~kpQkl#6LcgVJB)Zh=2Ks8+C~%et#c;OD?2T8>A&K+&oK~ zLcT#KJ}qn|H0M~oTF8i)*G5H|2#hlPs*8Kv zlv{D~qh)@j3KT=vLwLP2DD{=eZj!PP1!KCGXJnfTeIHAnx=#WSw&cU$AqX z3TN*)6KKkMgUS;+?^dpJ?><4*183r`iH|C7?N`X>BTnWB5q0AK3OU2*v6od|xLsX4 z>uN?+)@qNt^Xk+BJ}o39u&-?{c5cIM&_w zZN#9PbB+q=8_vKlb<9 zSb7+7g;DdLe6LBvNKnI8?%EVNYU$2h_YZw&ziftO{Fay-egy5i+HRj{nCN|h{lpQ3 zJlrFkcSvis@oi(uhrCK#2yl+2yJTUkzrRfVzQit-_N!)jMEu#&L@oA5-I=dJ7|r>B1<)AbSPM-e-6$KB{%}00h36@yOO+lXTOeIswizc!WPWy8i(Vm?0-amvIaVF-Y)t>P8s}@tL+srB)*l3)mM81Ak$pE=BKLXH#wGw;eKeB*~_d`i1V{x;vqFmTAGhcF`yH@DBu+ zW#?G)nJAto>)DLG+-`+sx2g>e2|-$tdHY<(cv@IE({#ie3N-X?xSLvBRi##W&{@7&wCNS1 z*tgSW>D9afl7E5>Yz#EBJ*p+T-O}u?C$4M_t~O6je(ic%!0VF{MK&JTpWht6X;Co8 zCk>a_zq$5fiSp4sKV}YED+DDK_9dUr(4t-zjdL&EryoTNA;>7b(K>(AIUfUb z_V*HdTzOm9kzDmlTk8w1`H>2P^Zf(5h zHTgH>Hd4*&Q#&WIfv)`i7QI3Sm-lL1-tExV9vb76mwUPD*6J^XK=HSAx$CTnABAHJ za|#z8Btm@R10#=h>y|bMkHpyiti*~1(uC~{!6^Y2u?=oJ%*jayC-=vXn%+RFl~KO@ zs5YW0fpFmq-*~K=?rF12vmEyUb?)aM3IqDxGkE&|=DuJP+DWgsXXd&SH@7L9LxZC+ zsPf}g@vz7-cr=z?u*7iXtUN>i_Ht#-bq89=PcK@UL9+!N-o|sDj;oMst~La1mX_%K z6aR>31p5d>ZwaBeic50`V@jHB``DBKC%kK)JAS252Kca&+ z!~>}Gt5W2-86OedmzRl~qpV$-FI_Py8bvQp^(8^wD16RRKOuaG0^6)!&G!T1_9tg- z@xQ{klMMUrx^}kuh>Gm~o2r%0XCsZzKjJ=xBj-X?p-^?tYWwenyxA6dSdFsR(U5V6f|dTQI!vs4Q<#rBYuId1@4H<~xR2b^EqpwwUW z`rqNy)r7KePq}0{61*3rbmVMh@dqJ*71F$OxGl7(gZ8ZFa{K+gsM#(7Qaff){Ouo^ z*A_`Oy2$IOgO@@j>a1r}z}2X0=KeRY9SC3mGceSF8SB6d-C!o>hWh4)CR#9;^3SF)c@-c^>^sC$f&EK;s0d}4Bhlkn!^mtVfz1T^7rJ@ R+5rQcJL_y$amF|Ce*nJiS(X3* literal 0 HcmV?d00001 diff --git a/examples/api/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-29x29@3x.png b/examples/api/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-29x29@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..e4adcbcedc3780375b853ef315244b28a735f047 GIT binary patch literal 5228 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 W{J%q-7Pk)mF@QP-t6ZsQ9q~WT1(vM< literal 0 HcmV?d00001 diff --git a/examples/api/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-40x40@1x.png b/examples/api/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-40x40@1x.png new file mode 100644 index 0000000000000000000000000000000000000000..2869541f735d2f3f890a5b400fcced58698cbac4 GIT binary patch literal 2216 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 literal 0 HcmV?d00001 diff --git a/examples/api/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-512@2x.png b/examples/api/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-512@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..704c929133f23779ee37099bf32d400b01599be1 GIT binary patch literal 121323 zcmeEug{m5g6SiNOwy&0@C$HTBK8?Bt|o8>^J=U z0l$CXd%<;WY%g9r&pGG5&pG$=K$Q9$C43xe8~^}-ucEA=2>^gmPr(2z5bEJ1@cS>+ z1EYnqrWye7ksSblh5`V$sF$F70Dz|e0I+Wg06b3z04Q9t+chLnZ(x|eR#E^UAHVWD z%aT!70#p=awS1Ni5%XzQ^PcP<1Kewx;ZmZ8&%c3{gV%ydRiZ3a3OQE|v|Bxt;B>2q zUr6KbE6P=im1%8Fm6U?AJ$yj$iIma8pQ|?9BjX0s(lGL<-%_XJdDN_uHqtdXo*NAa__0JDls!X0|&__N>fAEq;v;vayHQ zy3R`IWN|)oXSn@MmvfddOkByCe@9{n?+Klk_E|Yh@+sfQmWcX?Q+R<#Eek6){SeTN zl!yoUHojzC2WdbA+zJ=_ElP)2zwt?+JMiV97mt?y7LDO!WXSSUn_~(=)h-QD80ZJ= zu8eE~QEO;}pTz4#OW*V{0k-!J`Hrte+z{PfNMWd9Zd-?U9c<5^(g50|O5EVEbTdOJ zGnMlj8%rTL+6&X;Gc$7`vu^H!)eWx4u3ws~=w9kgfbV)gviB+Y0 zg2zvh{%Svd9KSg#>Tl}%A7-!B!<_&P!TpHkHNLYLBN}V0a(H&)46SfbqUzT-ZGx{? zopawUx5PN8Y~7MSWFbZ6s8xvK>Z9v%3IH;rg$10qf~m|P4eSz|NT3E0X?rx->2Cs~ zBo{m6FyM9F9bV$-f{a4w$6wQ1&7j>C|DH#(lQ?oxHLm@9+0dTEJ(_(Zl!S<952%xv zBC%fmx&Wq9wO({%RtweOc~vR!b4>G9^Y$E%-uyF5ktMV`ASvrlP+J&VVm?!=4MklO zp5R)i2QTR96eF-+PJ@YbwI~>a)ISsivI!EfEN=ius~5fc1<@|D0miWlzKfINVlA(D z1qHtfEw|f~;>IbL0sp4UqoOzdu!lo63VpSvL2%mB^UPbv7dO#;BFD~f=PC`kw-&x# z;5A#%KfG+Yc1i>#x1r`)GHew&mpj*ToDg>0S1~%?T_I&k*fdwWdOgb(!+do)nAU@`{f^!9w**eY&n=5l&QDkxn1X7YkYp-T|> zeSf9Zz-HFuiiIdD-zd$sjQ8!9rp#>}q`~T`kQ{T9LV3P_D6k)kXPVqJNL*oBep+dd zC6;K2Q;C=xJB$obY8Y}A35c}GC#7Ozs|e5MqSnfqvpSh_+G_WR(>1xx)1@Kun2?)KV$Dx~fA5o(HfgqQt>NIh z6z9AB$av)CRA$X<7R>tDY=kF+O$e8_k6F!(RfU8)u_H6Y%iDvn|MP)0lpYtbHm|L( zM+dLP1rQ`GRr(ZW-LL>IPR?aCqKPwKs->vgNZJAAfC@=MHotV)=y~Y!-hVOHGX19anYdY3v^B*zxT>UF zKK=C?;d?ZUX{&eNu@nk$uwn&(%=ye9d>KRfZz@wZ0$eY3+V1{54Z;e5e$6)mxExl% z&Sqr1)Wn2ek_!?M&Gzpw@o?NXnev&AR`#wsZo}k2*o`%BaS9?SvF86{5|G(rA|RT} z{{WerqG_OQSTGfAa9bG44Fi_2w%f_GQNNT|51sW4E{a$ak@jR!`Rk35?cSQKgKAM= zLFU$#zW0aW(;mA9`J)%>={vPSzHv9^)|a_W^O~Kl<#1*Wdo$-A$6q?E$LKjho)hk) zD(PSJ7K2Bs$oS50O{LnixqoPT-%T5t7upN!i}ca*?is~VeE#EX_(G<4I$*=VtN(tQ z#u=Z-B4oS2y+CBwhPMY_?cHkGulG-7I`XtbGRt}SF)^o=qx5jXDTT{P>wv`(0uIqn z8#j~mMV~5S9sHDLs2Fdcy!ugL(%C3g*DhICg;(_+!o!Ug@S*1!y!T&t-qd+}UGWpH zR;<>;PeRj!EbAP`;_BYB*8yqlIm+QyU2z{Vk3=*5e|0ord+2^Os;o-nx3x6N;|`6S1{0%{|1rcnJ`Fr#u4E4 zOkFwhfQzj%5a*?`9>ImA;fFcIJ3=w^ccvObzZl2Q2nX>-Ofm4O{sWRXT(CMRf1+#v zQnA!nxXS*(Pq9cX`n7CH8~HxGuPHc%nhReR{bQAo3uU%p;0IF?w_mOJ;=4|9|KelG z?6CN@TT;avb4mLx(lf*sFP%;GL(I)#CU4D<$Fm(r6$vsp#y4`u;Y_+wcjedCqj9>? zszE9XKZKh3Az^zBG`lX(r`!m-s`%FEU$=By@Xty7yoT@MPMP7Gd**hH1oP8pZTmLS z|6a(8fl{d{>!nKlVzuUNAfHm^ed))g(VS1P?9(+{U6obsAUuekx;y%CV;oqtKlrK^ zsb9l|W~p<~>qaH`%bQqY*D2|LHj9f^BROe04^5=!#FBw5Zl&88Em$^a_B&J*66uZ> zUR0MN!f6>^td6)>MTd$RJ=OpskT74?H7@|C8qmCwW8v8b#3QBdbz)4w(pmK9Ii8`H zk}`xt*8>@lFK$)&8MqVd(8c^tBD}vu$^5rJ7wqvHk`|&w1bWFSBSTQtOC^)ewCFYs ze3cx>N;8GO$G+*CvOCoceIWNU_)57WkbhO~Z}&t$9I|gxEt@16Karxs*uE^HBsFd6!Dtt@R=@N4xE}IE5u7-D}r8geNXsW`aHR?pM?} znFk=8=<;Uz=~`2A@+Ad?t2S2dHMBckHd>7WaQ? zziP-%dHFKzVU{Y&ApjDiWva86fEXpw|f9%1l&8JdaSVQ%3199>1I#;0db{Q z(Y6st2o1YfSzv(YB>|bdiEv~Jmi>FFrM(RFmQ0tmTZWi^rQ49FA-NYd63aBEb^T0o zkLK|H3oNWNYwHd@(dE;X8it$VM3gVN9(P&W+U}N+by`UP&G>TFI%$XDhJK=63I?-G zDCra+)pW94kuAUSPbrkyt11L(1Mb|7I)={f-q6|l$VEA^CRJ^Mqcy|0Nxz5^TpG?9d6Ck?+63ZqiXau#Zue5;eURr zr5iujyM$Lp;y~izDPtrA*2#lg#2;$kA653>+gYV0weY?*d#B6mOH>^<=Yi(%KI7rf zatT3%K7C2v`YzPpJA!r@;pxFF{XJmXkx8H+hziP-K2TK>_SqHE#~3PTRXx}E)Za%V z;MZ*eJUoFQ!oL6PdOxBl6S_{~0u%n%Y^>5$-jCX$&0Qi8RLa(8oy1vK1?ioXx~yT%pD zrz9YYMHJRUsv@HFH3<7Bn=lpmh;fM8zI>JO=fOWV^z!aZ_!G8*KZO3%v@74NV9!?) z0e&_@48yU-z4d(_VF&|0Js|&%Q@%uZP;+pc$9{Nh;>cc08k6RCbM&JI+`<0$bBX^} zSTXyKo+(RR&1?j5yyzkQRVM%R%st2A@5q88k&)q4LduLVDprL?(kEKa2(-9oft%J` z40#k3yMrWcpPKfhC8?caQNfrRt2$6U^9N1j5dXth$8VpOmojAPeP(E0Vv`(Y1CqE^ zv!#ZH{cEqqeWGI068WQyxGniN0OpCr`m)RpGSM2?M(lTHmAMZG99Q2+4qXPOG%l?@ zKge_-&_ETMDYCrE_;05967dhWPni3QI2qFRHK?S9c0Mgj*Scmmzx`{gfuW5bQ5FOf zeI_=aBX(7Ox@iI|pcb=;`?T4f-Vh@GCIS4n$I%+0HamlY=^ zIzPO>iG5&FHAJD*KP^CS@-f?1n582aTD}f(f%X0&L-qI_?+7~Xtwd%qD=qXfc=~@T zoU5J>?7w(S3bl;R=pB&-`!EOjQZd!D5iHC3#dWpgzn>9+_)C|Lnc!dxAR`c~BNLDU z(ct1y#u193%eS3QtRkM&U@*J+#zUWWg}|JC1y$^tJse^?BA!SgP$Q{un2 z7BAjNG=xX#(OxUJu9we39L@vS2VMdS2MTXR$q z+;5Hqc)zP0AD?%0W0Vn~n*(c>^IWiL$blbs=C+Z<4X;RP_*9-A3S{xiduAN zQ|!S7O}t?I4nhe$t>dS?Qpc({t-J%)Ovg{v+P;O494rJ5JqebyJawRBkmi)0xsamANJxnIFzk;#N@$gzw2BYZzVwxy$kFV!_sy>J)*+DP(M zjZSnojODf^t@HS?nvu0iNuS)9b^DKBx3ojSaf$TiK<=ks2FwDx`!;R@ylJYOqC9ja zpA;wd=7WevD!wMRY=EB>jMN#k@E3j}>C~}24HZdfI%0Y6WJxFIXQlP!zzHjHXBM#g zhW6iNfZ4k8^%aTcOIDjSUBQj~Udx7M+V8(i?#)KU5E4j!b8oO<|E(1N5Rr35d9&Si&1UZ?>wgect&O(!Qv#cWq%HkdE(l zyLw!v%)e#Acj;(8g;WtB)G&B#*CvnXcXeD&*st=2H=m79V)LbyzmA$B*3`HarerLJ zu3`ngz3AvDq+#$__`yG<96Gy^PuqmQpp?r(RlUA%kHudE$N4!-lIL*yfwxM-uV9%e zf7Sm_{{he#-vtIfc?52!w6(Q0=v`2`Txz2~aotl}iaY+{5$C({#?JeOB`>Ed;qhom z?H0fj*(uK(leFLPPu(36SkLCHt%G6VG8iO5RoZjWgOI`0FBBBNSsFJ|R`pEXQ-6cP2GVryU(Ob+H*?ZjrhezKR4-xg5((|wR$#1OG zmTd@y`I#v|prD^QF`=V>_akN&b2=F4xUp!Bl>`NnGp1;NXvJ#zX^3U|xYi;|1~+iG z_eQ2&91Uysu^h(7I3XGuvnRa_aqcjJ6N$$*!d(pSavs7+B!iL3{&gHjqUZkep=8YJ z2gir(9i6+6RJ~C8rnfZKTugEmTUKe^?(oi{tX@EVxH9Jx5bkHKrC^MhCr_+8(eS`uih4LxB&Zhr`Zv_lIvv0-`r(~I?sAv2dfqo`Z zJS1*&!F$11)H!hV=V(vxy#e_=)5}<+eAQsqPU?-77r#-(X2n*i?)5!)IaeZpGu{d@ ze_#SXk7?^1XfkQJ>(&47Y!z!z!%z78^m>j^-|Y8%tC1Mq1>(zmWvtDG$w<&tx_6;l9${!*c83T)iJS#9^w9!@t_`zqjRS;E+DLa5sd(l2&YO<7r^v*c!{ znoxJ|_3g5lTwKrjY>~g|KAFrQOJ~AOkI?)S(U49$_MN1vf9h;u*=%))#qZm#z5uN z^9s@p8no<=@ke0!nG%81W)MYSWno6Nxw)fF{oJnT&a$@i^C`&}iLI=bHtPaHBtPGi z1W2Ns9?@EbVc0E}|JN+~N+>fe#JJB~c6s#d35{e*U#YF$z(S6OjUps*J22awA@kAO zgzS9tx7O&|WQEX{o?afc)<6b!X`{-(C1;bCTRhSVpUPe4=r^T zU=J)BYF}#l#x^+k@@Dbx#(^v1-G{k|m++WW8njP}iSDicGz7v-ZR3709!liRf79e; zk~RAAgt#S;B{?tcMoji97!NY%M$abKJ8Kwt%Vn|)gC1OAP_mTN%FVucmheaa zt@g`rs1o;+-0iLg9+Oq1QJW9za9#yjg*1FXBuqGQfCV{ETKxFWv(_ObY; zsi)WQF+3S8K$#lh5BF}{rFe%j1v{tbZ>DGMLX@2W&YeMi>e-;)^rSODkitm&kAbEPc(#guo zXe7(%c^6`Eq;fd8e^_<>%j-C+{j}Nr14DJH7*=MsO1a&M0m?6cN7Hj0BC`Co84DCz zRm*ecV{#<@(K@;}C*bDi9&f#gn43OOQoT&qZeT<^sX|pO>XHs07Q6RUe$4Mxa63X5 zv(WkeI#~Pu4Cf~=ig{8+VnqZ2=Q|U4!9pH;3mBNiHr2LtDem?OV9YMd5J7eoAY>CT zYi}ag$=jwam;}JdAAX*Iz@BQmi}v7^-P;E`^eV`b%xn5G!KE|^bM3-SDis-)X~|Y| z2}a{I6oN6j$u?H19|sPY!v6CIl?n{(Y3TEQH#xuRRb-LTovY-vE59^}C(Z@cTwIW) z47W1KXKuaiju<@@q-DX%g%vW!efa+J%EL8k=C!WF?nOz4|95F|2J?afO=zl_;;9j; zN)i7fVtP<&M|_|iCs3uQ8S$|m9bF6L?jKs41hfV-#7;Bin{QIUgyza3ti#1lH~HvZlI z4*w2&(BzQjJBxXdBlp@ZrAj?rAjYz%>`ubl{69M~F`}m*K?-V#M#2)EJNph&qrTSd zdGk`afAn7IDJal(kZp8;vhQXd(J!IWPKD$uy6ujQIyhwAs|2kfv2oM;pmVGIAf+E?# z;78bV0Ax>YsgP7nJmYN3zC$$+oNtgt{F7wQBk;=YG0?x-_)jFbbL4r#y&@9@85K&t z*2$a`AG{V?PqofHzv2n>5D=#^Z~IQxqd=^lN3gZ>PpS3>Sw0oQfAsv1)Kj}{8>((-CZ%$P%J@t?=myMMMz?T0o z}aFlo5J4^by7(r3!t!pGO% zups#$AY*Vn^KFG*JwqnN?d6vnhs>EV59!)KNs8Skos=3-#>Hx?bAg^Vvd2t-nXooy z2%xhyVdje(wp~OtorlB?CMX^;$amsvDsT=8{o!9-l|^xTEJOx~MN=mzOEb(V+T8w) zGQhw6%G>qG!r%obpfshyMjf9XO?>|JF?$1ku{{74wB=_ZuLi!a!(KSg2hhR;#Q*D# zejXGbdyqkD{gPM&lKW_GYbFsXp1*}jNwsCr72BNik54+9Z*C?Dhu&WJ(Y%bUj|A9c zkVd9~vLE#k*u%^bTx5q*ixtwYQ7kN`aO8&LvyF$&R1p-ddX|cbf1oH_p*2h z$m?A&R!hFjm)S+cSz||MH(pj?z{C9?Z80n!{#cy9uYmB|APMickD`<}0~3&1b{4`) zY$HS-)9G;%e2B=tTTd8oWbl-q)yrVKh!5O-!-j+Ra}|d(GM9_YPZQ~?ZX%)k zDokwu_2eB%L zsql2P7B{6IcIUPP8XN}HiXn5CmZu6vN|~VKtT(ZNyDL9_FPMn@rNYo4XS<5A_ICbQ z?eu{-{q&3Pr8<@sUL?TQnmvH6C+F28J+Asy+DbnwN9C3^zW3U#>l$M18)8wslQzp= z7l4xt0_mxtc}%o%H=E*!G?)IGCCu~HQe`R2#|7gpY z84F~pC76OA6gy=+zT7=I|M22XfKaORP-6a{(0YN|)Vq(v-R`irf<%c-jlubN7)}-+ zOUk|F?MJLhK7A6FB$1D;XH^*`CDRUk<60K+2vPXwjO}c5h{i}{hsaWA8ioW?S1rKu zsPZ#BMwn1tnYEA~j;hE)f5QVnkU7)fSFk+Pyxn2qU%7C7?!KEBivxHz9>qvKYXR`K zd(^bNofBEr6k>Gc67kMIW)c{u95MXY9LTdIZIb~}e3oD^1636H=Skk8NT8ioKOmS9 zv%uf+7i@gDQFM}{p58e?GE`_l3ZNexRJew}^#<)9j)I(iN@rN`0LFWoJ8hQ`i#BIS znu!N_avd{uV6&mq9TBQH?n+VqodaW$u%bEQAZ)+;Od&stpNr8@bS;|H5bIf<^8#T_ z|4K^@GvH~TT;%FI^{knT4b2XOM?JE(|E4?qp54;Y>?zg2r|w#Jn7mMs%z!$zMT*{l z<${!$bCyGO*qmrr;wWz+1zJiMD#|$jqO84$mkKVe$IKTr>_Q5&A@PeoZX=o3Ml}lvT^WI4e3j1By>MpzMphGV#y*St+uj$g*R;hfCK)4kWWt( zkxcx9FKbfRLn!GWXJ={hY&MXmOqyo!H8IF);T2i2wn{$7Lyo(5B(^fxLN-=7(~aSh z0n@i$+2-6MFOP~~VgM3D*})5hTzWCWq5n8-f;q$eeE|k&o5w=1v(Fx>d-!1g%&+A? zKb(;Fb+F4XYZoXV;$kt8LacCEmF>5e0xnB(rUUszyL}(VSV{mgZ!YbG6Ndq?-hX2j z-LLHj3-#!haM2QB|CinYp+|u{qIt#fP2vs`D#Ib%pfT(U$U5l`J%s*|$w@yyD=P<)f&AQy5oN<2$o1*1h^qB) zEBPnnqdHs_a-eF@wcgSX%1<7&__`y185MzouVAb zD_seBJYKMJq*QnCCxIzbS!j$r*$YYNde<@gAZ>rnT&%PzMDiKPoA?5bS~rENOe7^& zzrc^Gy3Xd?R>$S0_mlDxOZQa+xkFMw{nvmK$6naLIZ8-qTb|_k@cLzB;uIw9iv4J9 zQj7z+dwapr{tZN;EL$zZXX5XV8opt<%%spM#^R=DDY!38bWDC$ALG$Kcxk@}B<^Ma ziRDk8LnRjfRmPmu{)d+qRed(89kxSUmof4<{R;&pAwd`wAMR$bbT}@FS3&Xg*tKjII8aCXvuFvGod& zO!xQJMV6MgngKO4Qa~ggeqE?t+lOgKY(_u>doec6_(r4exx`0U;8C2YAHX5$s{+=( zAXMV(KkHs77iHK%q5zr`Go5M6M+|A0$o9Dh)pI`aFmNgl>6X&c~rKXb6h>UPE)qq zl+&*$qvePW?%?fysQ8=Rv5v(FiunnUi!q&+H=65iwm(PxjuT?u{Qg=%&dgfX^nwfs z1D~M|&B4tUh2RVwE8R&ms~?u_k@~dvcYw)?xdBEqh)5LcF{(PP!a0>!TTD$M!RT40 zw=}xRL{E|J^nq18T-aE1AjMj=Z^=W)DBGk+S!}V#n63bnu17zdmnxA$lGpQQ=fMp` z7c%Htp^xa0VEWchm`_Sp8N^yK=jTy>b+nj)WU29Ys}3!!k5!F&Y@p8zX8}c_N+!=GZ+o|@! zy&jGLn&5&gq>G||V(I5St}z4LV`uFua3pJ9XRqJAFlpQCYA@RR0RthcrErQ$TEWM( z@LO;13N)*91u$k9bzCH0?MugTtTVLKas&T*^Fwp*yjyopvbLOZv*QCl+(wOeaxkT6 z$+G+ejP%VtY-IV-AY0uIDtG69FlG$RRbhppNJZ-30DdP=o&GXL8z%j>HE`A#QD%OO z$>PUSK6Q(&Js_7T2|r%@V(N>hvq4v|IW0Ef$>FKjsp3RzU<@B0${HdQyL&kw$VsTT zs+IwTYH-bH)Hynu(5ao_{ZX{y{}doesr2$Us$$%;cFJ7h*jg}1)zdbHPegP#NyxkUZluk|&uve3D*>P|MlVup zL-;5V3JT(re)t_o7zn$TKRveO04QKtCYl1PUM_b|-Os=tEF+FQ5Zr{$v4xzrj#tm2 z9ksqF13I*aR7VThngFKb0ZR{IMmJ?XCo=_|R9cY%=2Yr_FeGX6Hg@Llo8!IswbiKH z20UOpnq->4d4wzwm{`|e^o^E&{3v|=AiN22`fc;%_kW%Vc2qC_JG*;F$nblASv#H{ z=zBesbsxVO7CiGRG0a+1GhFscKET59s$Z`nGaKHQHa|q8(WH(-z+EOvjcqe9XB`j4 z)}%JQ1o9)`n@iHpV|m)kBwSVCfA)BMvz_mPy!vl?vFlC;THMoa*x})PFjP4O1&^!C zfSL1yQebs~KCj~d3pqa$0uEqt;&UV(tNlcgG9H&Z{od-4Jb^8KiR=RCAXc8^Pnt56 zp<4fOnF7(bgbec|eH*8q%=yQ#*U3Q}PBeB08SCBn-k=`Wbbf^^x4a3MjkaPFNnUFdMfRf4_)<9|O9lJ6rfw{N5^ zJgY?C$$^1|zf3jRLsfy=3JSrYjD^h1kZ59bHm?spdP4xC3S$C;kEs#9HP53gXJuFc zs_E!d0#h~_tfq=AjZ+|Z(s$H6$r$NEAzFVcC~>AO)@Miedw+-Q1=M|{jfj({r;%J} z?;#2gK17ENUCa&Lb3Bc=>kYX7aclF!hpuOJ`0;dLCnk$6m1;0C;Ec!Q7PI3lVQ>@9 z#jE$Tum{wz`|vhoR@H;e!*g$&Yt$(iPox|OewxU17{37kyo`4wSj3stl;@|gcQ=yW zSdvttL=zzmQNNJi3xkA!?K1ip?FlupwMMK=x+5#RUV>$`!1)=j+RS_Oz`@XJeUd5- z(I)ohm~8u<(s9Qu3XM$HDy8R@hXC6-gqF-Zo1%g+Y6WQBz4JUNE09z-AG;*RdF$jI zQv;XDM!0DY(pXZty|%QvZ)UsDINHnEXBhgW`hg0D|{m#K2yrCsb?rE6whMejDq2J6d(8`t8hNoE&*Df2Xdz|(9?Mle5j7*59K%vb3gm?YZ}472 zXGBU-0DTCZF)5>rECxvx&_EZ>eu^*AUN__yGcO}=GB!ha*|V2DSW7}LauFxniTNTmDm%r=}OshBE*pw-{e0g41^s`5)_x8 z>rBSSXwv}%!3n?>|Gvjqk0)7MS7@-Bw;jNB{_aZ;buuV9$lo<#MKhuZEQm3p?Gm!w4o!WbsIe!5v^1PZf}B4yz-G50d&p{{uK$uZ7TM zFHj7&C%4%tFZ)TayaZ(LA@yayByd=Enf;bG zBWZvuY#Cx0BQm|z^FAF+&IlQt<^_2e$%%X6Tv+)MLiw=GH6bBAtLf`Y>4@@{OAMhH z6Iy&sW-L^%QGu#K0UM}IFc`aXCRIAYTci#@!lUgV&6#tsn7_F2as`RqdViL*3dnuD{F|AZ79Bo}`$_iIv8v89>*IwkZs?_leb)+x45NzQ*bzo(HnOsN4 zYDI3TAM^b`+ra6uee0!`clIsr?s4a%C5Chdvr^t6bdfv|>PRRsQbvbiYIZHeiIk`V z@;Y>$P{fU6!j3O7rLNg@equQ=?>`Uf7>vM{XEJ!gjjD>5s1gK!WTVx{t%_r$033PX zG=H4vtiYtp77l%%2)JY!#b#Yqf2a{N0uUKYjL2f(D>7Tgl$L}g*}jz{-}-=#BnsX_~HC-w@s;9>9JAIU3iv%$g05E`AIpl%>A!q zEJiBBZih*deoUT!dD(*8wp>rMNPA&-b_0btyv?5&O|nZ+LD)Yvf(5C zZcJZgoo07X=X&%z*5&*5kK-ZvbZg}KhRBl@Y*%rL@(#i}l5-mSbA~1f0~I+sX^ILQ zoN@doGAEFpRf999$yC&iN>u8&W+WMO?91aZGIz<{UHmh$~$~nG-!(8qHlZKE&p~eT0Mza^uU)N|#Lc((^aSYm$L z8+Iq`%G8b`>?S@QKPmn@)sjd(cJnBJ*95#-JAKk#AlzeRyJ>T&D|X&kNf{YkZmP$O zg(~pK6P7(lgMik&`f+|I*WZdJX91Hp-z$7MS|y&1n|YdY>2mJ|WiSV0VMCa!aK9dKMj5g1jFAq4-$J-{uMDa*Q8`e>%a@)OS2wfG4}$ z&WM)IEB=F+0 zXZ=-SB0m({pu9`gfr1@E>w(P~-MmL%x{Qo=m;DXV2mm1p`5;_<0TN9Gh0x&8B0U3R z5^)p?!DhTR`E~pCAOA=kEh0PO^A453e0WK}AWnas+TB}o9>pNKObrEd9w4RQs=uHk z(ytzgDELmDq99#xwt|du`EL8r{t#d-Ao#jLD1vKPjhCw$9G;zGIb9-o-&MNww{?a;c{0wk^5g?pbc4~`{Xly-pwdyA<&*AMI4==e=zpeQ z9G_vFLfHAba*tVcF7 z2|sY+32w^}S-)?gN_BR$%<)ozm3|bf(&JGDQG(b46BLiotrlI*UnQ(1`KijF} z64~gxrkO;F>75@h#WU%O4V~2YrYP{%)7sl)Mv6(l_UJYHko#HDiH<*T#fynt0Ck

>qIa)CJDQwHr3JKGv}<_ybIUd;~Q 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 literal 0 HcmV?d00001 diff --git a/examples/api/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-60x60@3x.png b/examples/api/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-60x60@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..2a9fbc26ef1ef34cd7b0e4413f40d7912e3a777b GIT binary patch literal 11091 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 literal 0 HcmV?d00001 diff --git a/examples/api/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-76x76@1x.png b/examples/api/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-76x76@1x.png new file mode 100644 index 0000000000000000000000000000000000000000..2cdf18485f69316a5ae921f83997baa4c0042e50 GIT binary patch literal 4515 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 literal 0 HcmV?d00001 diff --git a/examples/api/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-76x76@2x.png b/examples/api/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-76x76@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..4723e4b45ea6377219fcf9475c2c75ae075fed5e GIT binary patch literal 9257 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 SjxJs>07Y3fXqB{C=zjqou7cM9 literal 0 HcmV?d00001 diff --git a/examples/api/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-83.5x83.5@2x.png b/examples/api/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-83.5x83.5@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..f26fee45c06c140414d4de5ef4a6911edb665b3e GIT binary patch literal 10219 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 + + + + method + debugging + + diff --git a/examples/api/src-tauri/gen/apple/LaunchScreen.storyboard b/examples/api/src-tauri/gen/apple/LaunchScreen.storyboard new file mode 100644 index 0000000..81b5f90 --- /dev/null +++ b/examples/api/src-tauri/gen/apple/LaunchScreen.storyboard @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/examples/api/src-tauri/gen/apple/Podfile b/examples/api/src-tauri/gen/apple/Podfile new file mode 100644 index 0000000..f4d3303 --- /dev/null +++ b/examples/api/src-tauri/gen/apple/Podfile @@ -0,0 +1,21 @@ +# Uncomment the next line to define a global platform for your project + +target 'api_iOS' do +platform :ios, '15.0' + # Pods for api_iOS +end + +target 'api_macOS' do +platform :osx, '11.0' + # Pods for api_macOS +end + +# Delete the deployment target for iOS and macOS, causing it to be inherited from the Podfile +post_install do |installer| + installer.pods_project.targets.each do |target| + target.build_configurations.each do |config| + config.build_settings.delete 'IPHONEOS_DEPLOYMENT_TARGET' + config.build_settings.delete 'MACOSX_DEPLOYMENT_TARGET' + end + end +end diff --git a/examples/api/src-tauri/gen/apple/Sources/api/bindings/bindings.h b/examples/api/src-tauri/gen/apple/Sources/api/bindings/bindings.h new file mode 100644 index 0000000..5152200 --- /dev/null +++ b/examples/api/src-tauri/gen/apple/Sources/api/bindings/bindings.h @@ -0,0 +1,8 @@ +#pragma once + +namespace ffi { + extern "C" { + void start_app(); + } +} + diff --git a/examples/api/src-tauri/gen/apple/Sources/api/main.mm b/examples/api/src-tauri/gen/apple/Sources/api/main.mm new file mode 100644 index 0000000..7793a9d --- /dev/null +++ b/examples/api/src-tauri/gen/apple/Sources/api/main.mm @@ -0,0 +1,6 @@ +#include "bindings/bindings.h" + +int main(int argc, char * argv[]) { + ffi::start_app(); + return 0; +} diff --git a/examples/api/src-tauri/gen/apple/api.xcodeproj/project.pbxproj b/examples/api/src-tauri/gen/apple/api.xcodeproj/project.pbxproj new file mode 100644 index 0000000..e01e3a6 --- /dev/null +++ b/examples/api/src-tauri/gen/apple/api.xcodeproj/project.pbxproj @@ -0,0 +1,469 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 54; + objects = { + +/* Begin PBXBuildFile section */ + 3043432501C9BC2DB6B4CB95 /* CoreGraphics.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 71EB788DE4662CFC0D97F567 /* CoreGraphics.framework */; }; + 328B4ADB3700C1873BEB7B10 /* main.mm in Sources */ = {isa = PBXBuildFile; fileRef = 90D3B673AFAB8D8AB561F616 /* main.mm */; }; + 6F379F15DA085785BA2624D4 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 6B7E79E23E646BA7968B457C /* Assets.xcassets */; }; + 832F9A55FEDEF3D807D8C40A /* libapp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 248286BAA086BB1A5F98B2B2 /* libapp.a */; }; + 9AADB041D25772D04E543F15 /* Metal.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 62601E25FA39E62BE119B74D /* Metal.framework */; }; + 9DDA3BE70DD0E4013973FE38 /* UIKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B6082E363D51372A7658C351 /* UIKit.framework */; }; + AC8BDC2C7A63FA3FDC5967F4 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 4B2D1B108AE002010BDEC6D2 /* LaunchScreen.storyboard */; }; + AFA0CA286325FD7A34968CA2 /* Security.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 384966E551417F94A02D2706 /* Security.framework */; }; + B60763BD194DFACA215EC7DA /* QuartzCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DC377692DC31A070A0188C9D /* QuartzCore.framework */; }; + C6D80743F168BDF017B7769E /* WebKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 59CFE20DCF760BE67D9CE3D6 /* WebKit.framework */; }; + DFFF888045C8D9D9FB69E8FD /* MetalKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 338E66700FD330B99D434DD7 /* MetalKit.framework */; }; + F86717F05E27C72C9FA1FB27 /* assets in Resources */ = {isa = PBXBuildFile; fileRef = 74A8FDFB350B966F5AAD4A24 /* assets */; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + 0E96CE07CD20273DD46BF325 /* main.rs */ = {isa = PBXFileReference; path = main.rs; sourceTree = ""; }; + 1C1AB1B414CA2795AFBEDDB9 /* tray.rs */ = {isa = PBXFileReference; path = tray.rs; sourceTree = ""; }; + 248286BAA086BB1A5F98B2B2 /* libapp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libapp.a; sourceTree = ""; }; + 2F63E2AA460089BB58D40C79 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = ""; }; + 338E66700FD330B99D434DD7 /* MetalKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = MetalKit.framework; path = System/Library/Frameworks/MetalKit.framework; sourceTree = SDKROOT; }; + 384966E551417F94A02D2706 /* Security.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Security.framework; path = System/Library/Frameworks/Security.framework; sourceTree = SDKROOT; }; + 4B2D1B108AE002010BDEC6D2 /* LaunchScreen.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = LaunchScreen.storyboard; sourceTree = ""; }; + 59CFE20DCF760BE67D9CE3D6 /* WebKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = WebKit.framework; path = System/Library/Frameworks/WebKit.framework; sourceTree = SDKROOT; }; + 5AC703CEBA41A121596066F3 /* api_iOS.app */ = {isa = PBXFileReference; includeInIndex = 0; lastKnownFileType = wrapper.application; path = api_iOS.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 62601E25FA39E62BE119B74D /* Metal.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Metal.framework; path = System/Library/Frameworks/Metal.framework; sourceTree = SDKROOT; }; + 6B7E79E23E646BA7968B457C /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 71EB788DE4662CFC0D97F567 /* CoreGraphics.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreGraphics.framework; path = System/Library/Frameworks/CoreGraphics.framework; sourceTree = SDKROOT; }; + 74A8FDFB350B966F5AAD4A24 /* assets */ = {isa = PBXFileReference; lastKnownFileType = folder; path = assets; sourceTree = SOURCE_ROOT; }; + 785D025E9542F7E098BF22B5 /* lib.rs */ = {isa = PBXFileReference; path = lib.rs; sourceTree = ""; }; + 879941AE3DAA14534BBC6391 /* api_iOS.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = api_iOS.entitlements; sourceTree = ""; }; + 90D3B673AFAB8D8AB561F616 /* main.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = main.mm; sourceTree = ""; }; + B6082E363D51372A7658C351 /* UIKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = UIKit.framework; path = System/Library/Frameworks/UIKit.framework; sourceTree = SDKROOT; }; + DC377692DC31A070A0188C9D /* QuartzCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = QuartzCore.framework; path = System/Library/Frameworks/QuartzCore.framework; sourceTree = SDKROOT; }; + EC8C7948C50C3C9B5D96CB61 /* bindings.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = bindings.h; sourceTree = ""; }; + F835F52713CE8F029D5D252C /* cmd.rs */ = {isa = PBXFileReference; path = cmd.rs; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 11E18DCDB3ADFE87C18915EF /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 832F9A55FEDEF3D807D8C40A /* libapp.a in Frameworks */, + 3043432501C9BC2DB6B4CB95 /* CoreGraphics.framework in Frameworks */, + 9AADB041D25772D04E543F15 /* Metal.framework in Frameworks */, + DFFF888045C8D9D9FB69E8FD /* MetalKit.framework in Frameworks */, + B60763BD194DFACA215EC7DA /* QuartzCore.framework in Frameworks */, + AFA0CA286325FD7A34968CA2 /* Security.framework in Frameworks */, + 9DDA3BE70DD0E4013973FE38 /* UIKit.framework in Frameworks */, + C6D80743F168BDF017B7769E /* WebKit.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 0677CEAF1F282F38CBA0F140 = { + isa = PBXGroup; + children = ( + 74A8FDFB350B966F5AAD4A24 /* assets */, + 6B7E79E23E646BA7968B457C /* Assets.xcassets */, + 4B2D1B108AE002010BDEC6D2 /* LaunchScreen.storyboard */, + F2116A6428EED18BE2A07E2B /* api_iOS */, + 86D903732E10FAC4D300E8DF /* Externals */, + 7A9A7AC155D9E22E54D6D847 /* Sources */, + CF9AA87D2F6E9C389B7AB70B /* src */, + 10C9FC3FA3E12D6A4A67999D /* Frameworks */, + 4AC51E67B71E27F15B02C5CD /* Products */, + ); + sourceTree = ""; + }; + 07051859D6E2D8109C8FB128 /* bindings */ = { + isa = PBXGroup; + children = ( + EC8C7948C50C3C9B5D96CB61 /* bindings.h */, + ); + path = bindings; + sourceTree = ""; + }; + 10C9FC3FA3E12D6A4A67999D /* Frameworks */ = { + isa = PBXGroup; + children = ( + 71EB788DE4662CFC0D97F567 /* CoreGraphics.framework */, + 248286BAA086BB1A5F98B2B2 /* libapp.a */, + 62601E25FA39E62BE119B74D /* Metal.framework */, + 338E66700FD330B99D434DD7 /* MetalKit.framework */, + DC377692DC31A070A0188C9D /* QuartzCore.framework */, + 384966E551417F94A02D2706 /* Security.framework */, + B6082E363D51372A7658C351 /* UIKit.framework */, + 59CFE20DCF760BE67D9CE3D6 /* WebKit.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; + 4AC51E67B71E27F15B02C5CD /* Products */ = { + isa = PBXGroup; + children = ( + 5AC703CEBA41A121596066F3 /* api_iOS.app */, + ); + name = Products; + sourceTree = ""; + }; + 7A9A7AC155D9E22E54D6D847 /* Sources */ = { + isa = PBXGroup; + children = ( + A3574F52DBC5463B9C3D043D /* api */, + ); + path = Sources; + sourceTree = ""; + }; + 86D903732E10FAC4D300E8DF /* Externals */ = { + isa = PBXGroup; + children = ( + ); + path = Externals; + sourceTree = ""; + }; + A3574F52DBC5463B9C3D043D /* api */ = { + isa = PBXGroup; + children = ( + 90D3B673AFAB8D8AB561F616 /* main.mm */, + 07051859D6E2D8109C8FB128 /* bindings */, + ); + path = api; + sourceTree = ""; + }; + CF9AA87D2F6E9C389B7AB70B /* src */ = { + isa = PBXGroup; + children = ( + F835F52713CE8F029D5D252C /* cmd.rs */, + 785D025E9542F7E098BF22B5 /* lib.rs */, + 0E96CE07CD20273DD46BF325 /* main.rs */, + 1C1AB1B414CA2795AFBEDDB9 /* tray.rs */, + ); + name = src; + path = ../../src; + sourceTree = ""; + }; + F2116A6428EED18BE2A07E2B /* api_iOS */ = { + isa = PBXGroup; + children = ( + 879941AE3DAA14534BBC6391 /* api_iOS.entitlements */, + 2F63E2AA460089BB58D40C79 /* Info.plist */, + ); + path = api_iOS; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 54DC6E273C78071F3BA12EF3 /* api_iOS */ = { + isa = PBXNativeTarget; + buildConfigurationList = 01CBC40275452376830D79B1 /* Build configuration list for PBXNativeTarget "api_iOS" */; + buildPhases = ( + FF948951157DE71465B5BD5F /* Build Rust Code */, + 71E73CC9AB5F1323EC1F6365 /* Sources */, + CA2BEC44B6EDA1F21B6155CD /* Resources */, + 11E18DCDB3ADFE87C18915EF /* Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = api_iOS; + packageProductDependencies = ( + ); + productName = api_iOS; + productReference = 5AC703CEBA41A121596066F3 /* api_iOS.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 9BC88C3717DA5D4B78A51C15 /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = YES; + LastUpgradeCheck = 1430; + TargetAttributes = { + 54DC6E273C78071F3BA12EF3 = { + DevelopmentTeam = Q93MBH6S2F; + }; + }; + }; + buildConfigurationList = 8FA67D0F928A09CD639137D1 /* Build configuration list for PBXProject "api" */; + compatibilityVersion = "Xcode 14.0"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + Base, + en, + ); + mainGroup = 0677CEAF1F282F38CBA0F140; + minimizedProjectReferenceProxies = 1; + preferredProjectObjectVersion = 54; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 54DC6E273C78071F3BA12EF3 /* api_iOS */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + CA2BEC44B6EDA1F21B6155CD /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 6F379F15DA085785BA2624D4 /* Assets.xcassets in Resources */, + AC8BDC2C7A63FA3FDC5967F4 /* LaunchScreen.storyboard in Resources */, + F86717F05E27C72C9FA1FB27 /* assets in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + FF948951157DE71465B5BD5F /* Build Rust Code */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + name = "Build Rust Code"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(SRCROOT)/Externals/x86_64/${CONFIGURATION}/libapp.a", + "$(SRCROOT)/Externals/arm64/${CONFIGURATION}/libapp.a", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "pnpm tauri ios xcode-script -v --platform ${PLATFORM_DISPLAY_NAME:?} --sdk-root ${SDKROOT:?} --framework-search-paths \"${FRAMEWORK_SEARCH_PATHS:?}\" --header-search-paths \"${HEADER_SEARCH_PATHS:?}\" --gcc-preprocessor-definitions \"${GCC_PREPROCESSOR_DEFINITIONS:-}\" --configuration ${CONFIGURATION:?} ${FORCE_COLOR} ${ARCHS:?}"; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 71E73CC9AB5F1323EC1F6365 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 328B4ADB3700C1873BEB7B10 /* main.mm in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin XCBuildConfiguration section */ + A83F70B4C02DD0222038C7F1 /* release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + SWIFT_VERSION = 5.0; + }; + name = release; + }; + B6AD77E490F315562F75D3D7 /* debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "$(inherited)", + "DEBUG=1", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = iphoneos; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + }; + name = debug; + }; + BF284FE6E7AE0C8DDCCE398B /* debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + ARCHS = ( + arm64, + ); + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CODE_SIGN_ENTITLEMENTS = api_iOS/api_iOS.entitlements; + CODE_SIGN_IDENTITY = "iPhone Developer"; + DEVELOPMENT_TEAM = "Q93MBH6S2F"; + ENABLE_BITCODE = NO; + "EXCLUDED_ARCHS[sdk=iphoneos*]" = x86_64; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "\".\"", + ); + INFOPLIST_FILE = api_iOS/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + "LIBRARY_SEARCH_PATHS[arch=arm64]" = "$(inherited) $(PROJECT_DIR)/Externals/arm64/$(CONFIGURATION) $(SDKROOT)/usr/lib/swift $(DEVELOPER_DIR)/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift/$(PLATFORM_NAME) $(DEVELOPER_DIR)/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift-5.0/$(PLATFORM_NAME)"; + "LIBRARY_SEARCH_PATHS[arch=x86_64]" = "$(inherited) $(PROJECT_DIR)/Externals/x86_64/$(CONFIGURATION) $(SDKROOT)/usr/lib/swift $(DEVELOPER_DIR)/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift/$(PLATFORM_NAME) $(DEVELOPER_DIR)/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift-5.0/$(PLATFORM_NAME)"; + PRODUCT_BUNDLE_IDENTIFIER = com.tauri.api; + PRODUCT_NAME = "Tauri API"; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + VALID_ARCHS = arm64; + }; + name = debug; + }; + DB0E254D0FD84970B57F6410 /* release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + ARCHS = ( + arm64, + ); + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CODE_SIGN_ENTITLEMENTS = api_iOS/api_iOS.entitlements; + CODE_SIGN_IDENTITY = "iPhone Developer"; + DEVELOPMENT_TEAM = "Q93MBH6S2F"; + ENABLE_BITCODE = NO; + "EXCLUDED_ARCHS[sdk=iphoneos*]" = x86_64; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "\".\"", + ); + INFOPLIST_FILE = api_iOS/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + "LIBRARY_SEARCH_PATHS[arch=arm64]" = "$(inherited) $(PROJECT_DIR)/Externals/arm64/$(CONFIGURATION) $(SDKROOT)/usr/lib/swift $(DEVELOPER_DIR)/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift/$(PLATFORM_NAME) $(DEVELOPER_DIR)/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift-5.0/$(PLATFORM_NAME)"; + "LIBRARY_SEARCH_PATHS[arch=x86_64]" = "$(inherited) $(PROJECT_DIR)/Externals/x86_64/$(CONFIGURATION) $(SDKROOT)/usr/lib/swift $(DEVELOPER_DIR)/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift/$(PLATFORM_NAME) $(DEVELOPER_DIR)/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift-5.0/$(PLATFORM_NAME)"; + PRODUCT_BUNDLE_IDENTIFIER = com.tauri.api; + PRODUCT_NAME = "Tauri API"; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + VALID_ARCHS = arm64; + }; + name = release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 01CBC40275452376830D79B1 /* Build configuration list for PBXNativeTarget "api_iOS" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + BF284FE6E7AE0C8DDCCE398B /* debug */, + DB0E254D0FD84970B57F6410 /* release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = debug; + }; + 8FA67D0F928A09CD639137D1 /* Build configuration list for PBXProject "api" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + B6AD77E490F315562F75D3D7 /* debug */, + A83F70B4C02DD0222038C7F1 /* release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = debug; + }; +/* End XCConfigurationList section */ + }; + rootObject = 9BC88C3717DA5D4B78A51C15 /* Project object */; +} diff --git a/examples/api/src-tauri/gen/apple/api.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/examples/api/src-tauri/gen/apple/api.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..919434a --- /dev/null +++ b/examples/api/src-tauri/gen/apple/api.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/examples/api/src-tauri/gen/apple/api.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/examples/api/src-tauri/gen/apple/api.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 0000000..ac90d5a --- /dev/null +++ b/examples/api/src-tauri/gen/apple/api.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,10 @@ + + + + + BuildSystemType + Original + DisableBuildSystemDeprecationDiagnostic + + + diff --git a/examples/api/src-tauri/gen/apple/api.xcodeproj/xcshareddata/xcschemes/api_iOS.xcscheme b/examples/api/src-tauri/gen/apple/api.xcodeproj/xcshareddata/xcschemes/api_iOS.xcscheme new file mode 100644 index 0000000..2a9f504 --- /dev/null +++ b/examples/api/src-tauri/gen/apple/api.xcodeproj/xcshareddata/xcschemes/api_iOS.xcscheme @@ -0,0 +1,123 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/examples/api/src-tauri/gen/apple/api_iOS/Info.plist b/examples/api/src-tauri/gen/apple/api_iOS/Info.plist new file mode 100644 index 0000000..7d35e70 --- /dev/null +++ b/examples/api/src-tauri/gen/apple/api_iOS/Info.plist @@ -0,0 +1,52 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + 2.0.0 + CFBundleVersion + 2.0.0 + LSRequiresIPhoneOS + + UILaunchStoryboardName + LaunchScreen + UIRequiredDeviceCapabilities + + arm64 + metal + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + NSCameraUsageDescription + Request camera access for WebRTC + NSMicrophoneUsageDescription + Request microphone access for WebRTC + NSFaceIDUsageDescription + Authenticate with biometrics + NFCReaderUsageDescription + Read and write to NFC tags for testing + + \ No newline at end of file diff --git a/examples/api/src-tauri/gen/apple/api_iOS/api_iOS.entitlements b/examples/api/src-tauri/gen/apple/api_iOS/api_iOS.entitlements new file mode 100644 index 0000000..0c67376 --- /dev/null +++ b/examples/api/src-tauri/gen/apple/api_iOS/api_iOS.entitlements @@ -0,0 +1,5 @@ + + + + + diff --git a/examples/api/src-tauri/gen/apple/project.yml b/examples/api/src-tauri/gen/apple/project.yml new file mode 100644 index 0000000..55c95ef --- /dev/null +++ b/examples/api/src-tauri/gen/apple/project.yml @@ -0,0 +1,89 @@ +name: api +options: + bundleIdPrefix: com.tauri.api + deploymentTarget: + iOS: 15.0 +fileGroups: [../../src] +configs: + debug: debug + release: release +settingGroups: + app: + base: + PRODUCT_NAME: Tauri API + PRODUCT_BUNDLE_IDENTIFIER: com.tauri.api + DEVELOPMENT_TEAM: Q93MBH6S2F +targetTemplates: + app: + type: application + sources: + - path: Sources + scheme: + environmentVariables: + RUST_BACKTRACE: full + RUST_LOG: info + settings: + groups: [app] +targets: + api_iOS: + type: application + platform: iOS + sources: + - path: Sources + - path: Assets.xcassets + - path: Externals + - path: api_iOS + - path: assets + buildPhase: resources + type: folder + - path: LaunchScreen.storyboard + info: + path: api_iOS/Info.plist + properties: + LSRequiresIPhoneOS: true + UILaunchStoryboardName: LaunchScreen + UIRequiredDeviceCapabilities: [arm64, metal] + UISupportedInterfaceOrientations: + - UIInterfaceOrientationPortrait + - UIInterfaceOrientationLandscapeLeft + - UIInterfaceOrientationLandscapeRight + UISupportedInterfaceOrientations~ipad: + - UIInterfaceOrientationPortrait + - UIInterfaceOrientationPortraitUpsideDown + - UIInterfaceOrientationLandscapeLeft + - UIInterfaceOrientationLandscapeRight + CFBundleShortVersionString: 2.0.0 + CFBundleVersion: 2.0.0 + entitlements: + path: api_iOS/api_iOS.entitlements + scheme: + environmentVariables: + RUST_BACKTRACE: full + RUST_LOG: info + settings: + base: + ENABLE_BITCODE: false + ARCHS: [arm64] + VALID_ARCHS: arm64 + LIBRARY_SEARCH_PATHS[arch=x86_64]: $(inherited) $(PROJECT_DIR)/Externals/x86_64/$(CONFIGURATION) $(SDKROOT)/usr/lib/swift $(DEVELOPER_DIR)/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift/$(PLATFORM_NAME) $(DEVELOPER_DIR)/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift-5.0/$(PLATFORM_NAME) + LIBRARY_SEARCH_PATHS[arch=arm64]: $(inherited) $(PROJECT_DIR)/Externals/arm64/$(CONFIGURATION) $(SDKROOT)/usr/lib/swift $(DEVELOPER_DIR)/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift/$(PLATFORM_NAME) $(DEVELOPER_DIR)/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift-5.0/$(PLATFORM_NAME) + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES: true + EXCLUDED_ARCHS[sdk=iphoneos*]: x86_64 + groups: [app] + dependencies: + - framework: libapp.a + embed: false + - sdk: CoreGraphics.framework + - sdk: Metal.framework + - sdk: MetalKit.framework + - sdk: QuartzCore.framework + - sdk: Security.framework + - sdk: UIKit.framework + - sdk: WebKit.framework + preBuildScripts: + - script: pnpm tauri ios xcode-script -v --platform ${PLATFORM_DISPLAY_NAME:?} --sdk-root ${SDKROOT:?} --framework-search-paths "${FRAMEWORK_SEARCH_PATHS:?}" --header-search-paths "${HEADER_SEARCH_PATHS:?}" --gcc-preprocessor-definitions "${GCC_PREPROCESSOR_DEFINITIONS:-}" --configuration ${CONFIGURATION:?} ${FORCE_COLOR} ${ARCHS:?} + name: Build Rust Code + basedOnDependencyAnalysis: false + outputFiles: + - $(SRCROOT)/Externals/x86_64/${CONFIGURATION}/libapp.a + - $(SRCROOT)/Externals/arm64/${CONFIGURATION}/libapp.a \ No newline at end of file diff --git a/examples/api/src-tauri/icons/128x128.png b/examples/api/src-tauri/icons/128x128.png new file mode 100644 index 0000000000000000000000000000000000000000..77e7d2338e9d8ccffc731198dc584906627c903f GIT binary patch literal 11059 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

j#(n6`cTK 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 DDbi4w literal 0 HcmV?d00001 diff --git a/examples/api/src-tauri/icons/tray_icon_with_transparency.png b/examples/api/src-tauri/icons/tray_icon_with_transparency.png new file mode 100644 index 0000000000000000000000000000000000000000..99d8ff2bb4532690998f5c50b38206b5fec780ba GIT binary patch literal 1015 zcmVNxWUnbcmD%wRJvBA8hhUtjwZ;x$X=!QIFpLd2nU2y$FqKB5Q5mI^5M#*l z^74AG*W1LGQkpoQVRMnVH!@#sw_&EuBv9pPZZ=MA;yS2H}TIk$s?*!`Pv&mlQF?WAskgymu6Z zegwgbJp`~y!s53?J)e$x6?{Czs3Jktjf?4*MUf zeIx>KY{9`6h&4~*>2nxgmwh;RS}K)V)oOJBHL^dGZEz>AuRk&XW~VwpCPO5;xw-Kz zsE?&mDFtV+X@BGVb?5 zrqA_y{gbXoLP7T(vMwV)X19;?eyjylkZo*HD^>vBMIx`-FG0?gAc(?|2uTEM^)Dps z{}qC(={jbB5i=PW@d%0u#Y?0wKq4^6AOs4kkzUcr4Z!^b+v9=*dmaKl0f9GRFB3UW zCX<1Q_z=L}-&49ni}NmEF=7eE?_Fg0QeHxej)fp!LuXBnA<#-#X%0OF6lSRfx2Wp? zjr(e!gk*372d4Q!y%%&Dc@JbV8J_8y5|FR>xUL(V8DIzN-v<$IWB{rYWK2E`qDKY% zS}Yb9dE=3w;?J?GX^va4t3)}L;(HHpPqDXeyv43Sp0|1d2`fI@d;#;eh!9^fVgW%d zClTkpmrp`}(NKqMArp!ha!dq zmSetLP)2*K;oi_nHk-}iRddGDtm^XE&Cbs9wKRBw;qv+j^e5whDj?hj5g0Nsj3&6z zXFi-30-oi&AYkmGFz&yi*a2w4 lWDrQ2Z3Odc5~u$~mEST+7A)-%9(4c!002ovPDHLkV1i&3(Psbv literal 0 HcmV?d00001 diff --git a/examples/api/src-tauri/locales/pt-BR.wxl b/examples/api/src-tauri/locales/pt-BR.wxl new file mode 100644 index 0000000..8f58c07 --- /dev/null +++ b/examples/api/src-tauri/locales/pt-BR.wxl @@ -0,0 +1,6 @@ + + Executar Tauri API + Uma versão mais recente de Tauri API está instalada. + Adiciona o caminho do executável de Tauri API para a variável de ambiente PATH. Isso permite Tauri API ser executado pela linha de comando. + Instala Tauri API. + diff --git a/examples/api/src-tauri/src/cmd.rs b/examples/api/src-tauri/src/cmd.rs new file mode 100644 index 0000000..734552c --- /dev/null +++ b/examples/api/src-tauri/src/cmd.rs @@ -0,0 +1,24 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use serde::Deserialize; +use tauri::command; + +#[derive(Debug, Deserialize)] +#[allow(unused)] +pub struct RequestBody { + id: i32, + name: String, +} + +#[command] +pub fn log_operation(event: String, payload: Option) { + log::info!("{} {:?}", event, payload); +} + +#[command] +pub fn perform_request(endpoint: String, body: RequestBody) -> String { + println!("{} {:?}", endpoint, body); + "message response".into() +} diff --git a/examples/api/src-tauri/src/lib.rs b/examples/api/src-tauri/src/lib.rs new file mode 100644 index 0000000..9a20067 --- /dev/null +++ b/examples/api/src-tauri/src/lib.rs @@ -0,0 +1,223 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +mod cmd; +#[cfg(desktop)] +mod tray; + +use serde::Serialize; +use tauri::{ + webview::{PageLoadEvent, WebviewWindowBuilder}, + App, AppHandle, Emitter, Listener, Manager, RunEvent, WebviewUrl, +}; + +#[derive(Clone, Serialize)] +struct Reply { + data: String, +} + +pub type SetupHook = Box Result<(), Box> + Send>; +pub type OnEvent = Box; + +#[cfg_attr(mobile, tauri::mobile_entry_point)] +pub fn run() { + #[allow(unused_mut)] + let mut builder = tauri::Builder::default(); + + // WebDriver automation bridge for the plugins e2e suite (packages/api-e2e). + // Registered as early as possible per the plugin's docs, behind the + // off-by-default `automation` feature. + #[cfg(all(desktop, feature = "automation"))] + { + builder = builder.plugin(tauri_plugin_automation::init()); + } + + #[allow(unused_mut)] + let mut builder = builder + .plugin( + tauri_plugin_log::Builder::default() + .level(log::LevelFilter::Info) + // forward records to the webview so `attachLogger`/`attachConsole` work + .target(tauri_plugin_log::Target::new( + tauri_plugin_log::TargetKind::Webview, + )) + .build(), + ) + .plugin(tauri_plugin_fs::init()) + .plugin(tauri_plugin_clipboard_manager::init()) + .plugin(tauri_plugin_dialog::init()) + .plugin(tauri_plugin_http::init()) + .plugin(tauri_plugin_notification::init()) + .plugin(tauri_plugin_os::init()) + .plugin(tauri_plugin_process::init()) + .plugin(tauri_plugin_opener::init()) + .plugin(tauri_plugin_shell::init()) + .plugin(tauri_plugin_store::Builder::default().build()) + .plugin(tauri_plugin_upload::init()) + .plugin(tauri_plugin_deep_link::init()) + .plugin(tauri_plugin_websocket::init()) + .plugin( + tauri_plugin_sql::Builder::new() + .add_migrations( + "sqlite:api.db", + vec![tauri_plugin_sql::Migration { + version: 1, + description: "create_todos_table", + sql: "CREATE TABLE todos (id INTEGER PRIMARY KEY AUTOINCREMENT, title TEXT NOT NULL, done INTEGER NOT NULL DEFAULT 0);", + kind: tauri_plugin_sql::MigrationKind::Up, + }], + ) + .build(), + ) + .setup(move |app| { + // the argon2 salt lives next to the snapshots the frontend creates + let local_data_dir = app.path().app_local_data_dir()?; + std::fs::create_dir_all(&local_data_dir)?; + app.handle().plugin( + tauri_plugin_stronghold::Builder::with_argon2(&local_data_dir.join("salt.txt")) + .build(), + )?; + + #[cfg(desktop)] + { + // registered before the tray, whose events it tracks + app.handle().plugin(tauri_plugin_positioner::init())?; + tray::create_tray(app.handle())?; + app.handle().plugin(tauri_plugin_cli::init())?; + app.handle().plugin(tauri_plugin_autostart::init( + tauri_plugin_autostart::MacosLauncher::LaunchAgent, + None, + ))?; + app.handle() + .plugin(tauri_plugin_global_shortcut::Builder::new().build())?; + app.handle() + .plugin(tauri_plugin_window_state::Builder::new().build())?; + app.handle() + .plugin(tauri_plugin_updater::Builder::new().build())?; + } + #[cfg(mobile)] + { + app.handle().plugin(tauri_plugin_barcode_scanner::init())?; + app.handle().plugin(tauri_plugin_nfc::init())?; + app.handle().plugin(tauri_plugin_biometric::init())?; + app.handle().plugin(tauri_plugin_geolocation::init())?; + app.handle().plugin(tauri_plugin_haptics::init())?; + } + + let mut webview_window_builder = + WebviewWindowBuilder::new(app, "main", WebviewUrl::default()); + #[cfg(desktop)] + { + webview_window_builder = webview_window_builder + .user_agent(&format!("Tauri API - {}", std::env::consts::OS)) + .title("Tauri API Validation") + .inner_size(1000., 800.) + .min_inner_size(600., 400.) + .visible(false); + } + + #[cfg(target_os = "windows")] + { + webview_window_builder = webview_window_builder + .transparent(true) + .shadow(true) + .decorations(false); + } + + #[cfg(target_os = "macos")] + { + webview_window_builder = webview_window_builder.transparent(true); + } + + let _webview = webview_window_builder.build().unwrap(); + + std::thread::spawn(|| { + let server = match tiny_http::Server::http("localhost:3003") { + Ok(s) => s, + Err(e) => { + eprintln!("{}", e); + std::process::exit(1); + } + }; + loop { + if let Ok(mut request) = server.recv() { + let mut body = Vec::new(); + let _ = request.as_reader().read_to_end(&mut body); + let mut headers = request.headers().to_vec(); + + if !headers.iter().any(|header| header.field == tiny_http::HeaderField::from_bytes(b"Cookie").unwrap()) { + let expires = time::OffsetDateTime::now_utc() + time::Duration::days(1); + // RFC 1123 format + let format = time::macros::format_description!( + "[weekday repr:short], [day] [month repr:short] [year] [hour]:[minute]:[second] GMT" + ); + let expires_str = expires.format(format).unwrap(); + headers.push( + tiny_http::Header::from_bytes( + &b"Set-Cookie"[..], + format!("session-token=test-value; Secure; Path=/; Expires={expires_str}") + .as_bytes(), + ) + .unwrap(), + ); + } + + let response = tiny_http::Response::new( + tiny_http::StatusCode(200), + headers, + std::io::Cursor::new(body), + request.body_length(), + None, + ); + let _ = request.respond(response); + } + } + }); + + Ok(()) + }) + .on_page_load(|webview, payload| { + if payload.event() == PageLoadEvent::Finished { + let webview_ = webview.clone(); + webview.listen("js-event", move |event| { + println!("got js-event with message '{:?}'", event.payload()); + let reply = Reply { + data: "something else".to_string(), + }; + + webview_ + .emit("rust-event", Some(reply)) + .expect("failed to emit"); + }); + } + }); + + #[cfg(target_os = "macos")] + { + builder = builder.menu(tauri::menu::Menu::default); + } + + #[allow(unused_mut)] + let mut app = builder + .invoke_handler(tauri::generate_handler![ + cmd::log_operation, + cmd::perform_request, + ]) + .build(tauri::generate_context!()) + .expect("error while building tauri application"); + + #[cfg(target_os = "macos")] + app.set_activation_policy(tauri::ActivationPolicy::Regular); + + app.run(move |_app_handle, _event| { + #[cfg(desktop)] + if let RunEvent::ExitRequested { code, api, .. } = &_event { + if code.is_none() { + // Keep the event loop running even if all windows are closed + // This allow us to catch system tray events when there is no window + api.prevent_exit(); + } + } + }) +} diff --git a/examples/api/src-tauri/src/main.rs b/examples/api/src-tauri/src/main.rs new file mode 100644 index 0000000..bbed006 --- /dev/null +++ b/examples/api/src-tauri/src/main.rs @@ -0,0 +1,10 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +// Prevents additional console window on Windows in release, DO NOT REMOVE!! +#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] + +fn main() { + api_lib::run(); +} diff --git a/examples/api/src-tauri/src/tray.rs b/examples/api/src-tauri/src/tray.rs new file mode 100644 index 0000000..ba4a5a0 --- /dev/null +++ b/examples/api/src-tauri/src/tray.rs @@ -0,0 +1,128 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use std::sync::atomic::{AtomicBool, Ordering}; +use tauri::{ + menu::{Menu, MenuItem}, + tray::{MouseButton, MouseButtonState, TrayIconBuilder, TrayIconEvent}, + Manager, Runtime, WebviewUrl, WebviewWindowBuilder, +}; + +pub fn create_tray(app: &tauri::AppHandle) -> tauri::Result<()> { + let toggle_i = MenuItem::with_id(app, "toggle", "Toggle", true, None::<&str>)?; + let new_window_i = MenuItem::with_id(app, "new-window", "New window", true, None::<&str>)?; + let icon_i_1 = MenuItem::with_id(app, "icon-1", "Icon 1", true, None::<&str>)?; + let icon_i_2 = MenuItem::with_id(app, "icon-2", "Icon 2", true, None::<&str>)?; + #[cfg(target_os = "macos")] + let set_title_i = MenuItem::with_id(app, "set-title", "Set Title", true, None::<&str>)?; + let switch_i = MenuItem::with_id(app, "switch-menu", "Switch Menu", true, None::<&str>)?; + let quit_i = MenuItem::with_id(app, "quit", "Quit", true, None::<&str>)?; + let remove_tray_i = + MenuItem::with_id(app, "remove-tray", "Remove Tray icon", true, None::<&str>)?; + let menu1 = Menu::with_items( + app, + &[ + &toggle_i, + &new_window_i, + &icon_i_1, + &icon_i_2, + #[cfg(target_os = "macos")] + &set_title_i, + &switch_i, + &quit_i, + &remove_tray_i, + ], + )?; + let menu2 = Menu::with_items( + app, + &[&toggle_i, &new_window_i, &switch_i, &quit_i, &remove_tray_i], + )?; + + let is_menu1 = AtomicBool::new(true); + + let _ = TrayIconBuilder::with_id("tray-1") + .tooltip("Tauri") + .icon(app.default_window_icon().unwrap().clone()) + .menu(&menu1) + .show_menu_on_left_click(false) + .on_menu_event(move |app, event| match event.id.as_ref() { + "quit" => { + app.exit(0); + } + "remove-tray" => { + app.remove_tray_by_id("tray-1"); + } + "toggle" => { + if let Some(window) = app.get_webview_window("main") { + let new_title = if window.is_visible().unwrap_or_default() { + let _ = window.hide(); + "Show" + } else { + let _ = window.show(); + let _ = window.set_focus(); + "Hide" + }; + toggle_i.set_text(new_title).unwrap(); + } + } + "new-window" => { + let _ = WebviewWindowBuilder::new(app, "new", WebviewUrl::App("index.html".into())) + .title("Tauri") + .build(); + } + #[cfg(target_os = "macos")] + "set-title" => { + if let Some(tray) = app.tray_by_id("tray-1") { + let _ = tray.set_title(Some("Tauri")); + } + } + i @ "icon-1" | i @ "icon-2" => { + if let Some(tray) = app.tray_by_id("tray-1") { + let _ = tray.set_icon(Some(if i == "icon-1" { + tauri::image::Image::from_bytes(include_bytes!("../icons/icon.ico")) + .unwrap() + } else { + tauri::image::Image::from_bytes(include_bytes!( + "../icons/tray_icon_with_transparency.png" + )) + .unwrap() + })); + } + } + "switch-menu" => { + let flag = is_menu1.load(Ordering::Relaxed); + let (menu, tooltip) = if flag { + (menu2.clone(), "Menu 2") + } else { + (menu1.clone(), "Tauri") + }; + if let Some(tray) = app.tray_by_id("tray-1") { + let _ = tray.set_menu(Some(menu)); + let _ = tray.set_tooltip(Some(tooltip)); + } + is_menu1.store(!flag, Ordering::Relaxed); + } + + _ => {} + }) + .on_tray_icon_event(|tray, event| { + // lets the positioner plugin place windows relative to the tray icon + tauri_plugin_positioner::on_tray_event(tray.app_handle(), &event); + if let TrayIconEvent::Click { + button_state: MouseButtonState::Down, + button: MouseButton::Left, + .. + } = event + { + let app = tray.app_handle(); + if let Some(window) = app.get_webview_window("main") { + let _ = window.show(); + let _ = window.set_focus(); + } + } + }) + .build(app); + + Ok(()) +} diff --git a/examples/api/src-tauri/tauri.conf.json b/examples/api/src-tauri/tauri.conf.json new file mode 100644 index 0000000..e30e336 --- /dev/null +++ b/examples/api/src-tauri/tauri.conf.json @@ -0,0 +1,113 @@ +{ + "$schema": "../node_modules/@tauri-apps/cli/config.schema.json", + "productName": "Tauri API", + "version": "2.0.0", + "identifier": "com.tauri.api", + "build": { + "devUrl": "http://localhost:5173", + "frontendDist": "../dist", + "beforeDevCommand": "pnpm dev", + "beforeBuildCommand": "pnpm build" + }, + "app": { + "withGlobalTauri": true, + "macOSPrivateApi": true, + "security": { + "pattern": { + "use": "isolation", + "options": { + "dir": "../isolation-dist/" + } + }, + "csp": { + "default-src": "'self' customprotocol: asset:", + "connect-src": "ipc: http://ipc.localhost", + "font-src": ["https://fonts.gstatic.com"], + "img-src": "'self' asset: http://asset.localhost blob: data:", + "style-src": "'unsafe-inline' 'self' http://fonts.googleapis.com" + }, + "freezePrototype": true, + "assetProtocol": { + "enable": true, + "scope": { + "allow": ["$APPDATA/db/**", "$RESOURCE/**"], + "deny": ["$APPDATA/db/*.stronghold"] + } + } + } + }, + "plugins": { + "cli": { + "description": "Tauri API example", + "args": [ + { + "short": "c", + "name": "config", + "takesValue": true, + "description": "Config path" + }, + { + "short": "t", + "name": "theme", + "takesValue": true, + "description": "App theme", + "possibleValues": ["light", "dark", "system"] + }, + { + "short": "v", + "name": "verbose", + "description": "Verbosity level" + } + ], + "subcommands": { + "update": { + "description": "Updates the app", + "args": [ + { + "short": "b", + "name": "background", + "description": "Update in background" + } + ] + } + } + }, + "shell": { + "open": true + }, + "updater": { + "pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IDE5QzMxNjYwNTM5OEUwNTgKUldSWTRKaFRZQmJER1h4d1ZMYVA3dnluSjdpN2RmMldJR09hUFFlZDY0SlFqckkvRUJhZDJVZXAK", + "dangerousInsecureTransportProtocol": true, + "endpoints": [ + "http://localhost:5173/updater-test/updater.json", + "https://tauri-update-server.vercel.app/update/{{target}}/{{current_version}}" + ] + } + }, + "bundle": { + "active": true, + "icon": [ + "icons/32x32.png", + "icons/128x128.png", + "icons/128x128@2x.png", + "icons/icon.icns", + "icons/icon.ico" + ], + "windows": { + "wix": { + "language": { + "en-US": {}, + "pt-BR": { + "localePath": "locales/pt-BR.wxl" + } + } + }, + "nsis": { + "compression": "none" + } + }, + "iOS": { + "minimumSystemVersion": "15.0" + } + } +} diff --git a/examples/api/src/App.svelte b/examples/api/src/App.svelte new file mode 100644 index 0000000..573ff7e --- /dev/null +++ b/examples/api/src/App.svelte @@ -0,0 +1,503 @@ + + + + + +{#if isWindows} + +{/if} + + + + + diff --git a/examples/api/src/app.css b/examples/api/src/app.css new file mode 100644 index 0000000..f411d6e --- /dev/null +++ b/examples/api/src/app.css @@ -0,0 +1,84 @@ +:root { + line-height: 1.5; +} + +:root.dark { + color-scheme: dark; +} +:root.light { + color-scheme: light; +} + +*:not(h1, h2, h3, h4, h5, h6) { + margin: 0; + padding: 0; +} + +* { + box-sizing: border-box; + font-family: 'Rubik', sans-serif; +} + +button { + color: inherit; + background-color: inherit; +} + +::-webkit-scrollbar { + width: 0.25rem; + height: 3px; +} + +::-webkit-scrollbar-track { + background: transparent; +} + +::-webkit-scrollbar-thumb { + border-radius: 0.25rem; +} + +code { + padding: 0.05rem 0.25rem; +} + +code.code-block { + padding: 0.5rem; +} + +#sidebar { + width: 18.75rem; +} + +@media screen and (width < 640px) { + #sidebar { + --translate-x: -18.75rem; + transform: translateX(var(--translate-x)); + } +} + +#sidebar, +main { + padding-top: calc(env(safe-area-inset-top) + 2rem); +} + +#sidebar { + padding-left: calc(env(safe-area-inset-left) + 0.5rem); +} + +main { + padding-right: env(safe-area-inset-right); +} + +#sidebarToggle { + margin-top: calc(env(safe-area-inset-top) + 0.5rem); + margin-left: calc(env(safe-area-inset-left) + 0.5rem); +} + +#sidebar, +#console { + padding-bottom: calc(env(safe-area-inset-bottom) + 1.5rem); +} + +.transparent { + background-color: transparent; +} diff --git a/examples/api/src/lib/TitleBar.svelte b/examples/api/src/lib/TitleBar.svelte new file mode 100644 index 0000000..c1e4253 --- /dev/null +++ b/examples/api/src/lib/TitleBar.svelte @@ -0,0 +1,103 @@ + + +
+ Tauri API Validation + + + + + + +
diff --git a/examples/api/src/lib/utils.js b/examples/api/src/lib/utils.js new file mode 100644 index 0000000..a5ddc05 --- /dev/null +++ b/examples/api/src/lib/utils.js @@ -0,0 +1,15 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +export function arrayBufferToBase64(buffer, callback) { + const blob = new Blob([buffer], { + type: 'application/octet-binary' + }) + const reader = new FileReader() + reader.onload = function (evt) { + const dataurl = evt.target.result + callback(dataurl.substr(dataurl.indexOf(',') + 1)) + } + reader.readAsDataURL(blob) +} diff --git a/examples/api/src/main.ts b/examples/api/src/main.ts new file mode 100644 index 0000000..c0e0e5f --- /dev/null +++ b/examples/api/src/main.ts @@ -0,0 +1,14 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +import 'uno.css' +import './app.css' +import App from './App.svelte' +import { mount } from 'svelte' + +const app = mount(App, { + target: document.querySelector('#app')! +}) + +export default app diff --git a/examples/api/src/views/Autostart.svelte b/examples/api/src/views/Autostart.svelte new file mode 100644 index 0000000..1f8c557 --- /dev/null +++ b/examples/api/src/views/Autostart.svelte @@ -0,0 +1,30 @@ + + +
+ + Launching at login is {enabled ? 'enabled' : 'disabled'} +
diff --git a/examples/api/src/views/Biometric.svelte b/examples/api/src/views/Biometric.svelte new file mode 100644 index 0000000..3f287b0 --- /dev/null +++ b/examples/api/src/views/Biometric.svelte @@ -0,0 +1,30 @@ + + +
+ + +
+ diff --git a/examples/api/src/views/Cli.svelte b/examples/api/src/views/Cli.svelte new file mode 100644 index 0000000..06f38e0 --- /dev/null +++ b/examples/api/src/views/Cli.svelte @@ -0,0 +1,29 @@ + + +
+ This binary can be run from the terminal and takes the following arguments: + +
+  --config <PATH>
+  --theme <light|dark|system>
+  --verbose
+
+ Additionally, it has a update --background subcommand. +
+
+
+ Note that the arguments are only parsed, not implemented. +
+
+
+ diff --git a/examples/api/src/views/Clipboard.svelte b/examples/api/src/views/Clipboard.svelte new file mode 100644 index 0000000..2bc0fcc --- /dev/null +++ b/examples/api/src/views/Clipboard.svelte @@ -0,0 +1,68 @@ + + +
+ + + + +
diff --git a/examples/api/src/views/Communication.svelte b/examples/api/src/views/Communication.svelte new file mode 100644 index 0000000..f34dcd3 --- /dev/null +++ b/examples/api/src/views/Communication.svelte @@ -0,0 +1,52 @@ + + +
+ + + +
diff --git a/examples/api/src/views/DeepLink.svelte b/examples/api/src/views/DeepLink.svelte new file mode 100644 index 0000000..9a1a191 --- /dev/null +++ b/examples/api/src/views/DeepLink.svelte @@ -0,0 +1,67 @@ + + +
+
+ Current deep link: {current ? current.join(', ') : 'none'} +
+ +
+ + + + +
+
diff --git a/examples/api/src/views/Dialog.svelte b/examples/api/src/views/Dialog.svelte new file mode 100644 index 0000000..7c1b616 --- /dev/null +++ b/examples/api/src/views/Dialog.svelte @@ -0,0 +1,195 @@ + + +
+ + +
+ +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ +
+ + + + + + +
diff --git a/examples/api/src/views/FileSystem.svelte b/examples/api/src/views/FileSystem.svelte new file mode 100644 index 0000000..bc35547 --- /dev/null +++ b/examples/api/src/views/FileSystem.svelte @@ -0,0 +1,272 @@ + + +
+ {#if isMobile} +
+ On mobile, paths outside of App* paths require the use of dialogs + regardless of Tauri's scope mechanism. +
+
+ {/if} +
+ + +
+
+
+ + + + +
+ + +
+ +
+ {#if file} +
+ + + +
+ {/if} + +

Watch

+ + +
+
+ + +
+
+
+ + +
+
+
+ + +
+
+ +
+ + diff --git a/examples/api/src/views/Geolocation.svelte b/examples/api/src/views/Geolocation.svelte new file mode 100644 index 0000000..cd181df --- /dev/null +++ b/examples/api/src/views/Geolocation.svelte @@ -0,0 +1,29 @@ + + + diff --git a/examples/api/src/views/Haptics.svelte b/examples/api/src/views/Haptics.svelte new file mode 100644 index 0000000..9ddf15c --- /dev/null +++ b/examples/api/src/views/Haptics.svelte @@ -0,0 +1,46 @@ + + +
+ + + + + +
+ +
+ +

+ Depending on your device settings for haptic feedback some of the buttons may + not work. +

diff --git a/examples/api/src/views/Http.svelte b/examples/api/src/views/Http.svelte new file mode 100644 index 0000000..5c7c3c4 --- /dev/null +++ b/examples/api/src/views/Http.svelte @@ -0,0 +1,103 @@ + + +
+ +
+ +
+ +
+ +
+ +

HTTP Form

+ +
+ + +
+
+
+ +
+
+ diff --git a/examples/api/src/views/Nfc.svelte b/examples/api/src/views/Nfc.svelte new file mode 100644 index 0000000..4d7ea2c --- /dev/null +++ b/examples/api/src/views/Nfc.svelte @@ -0,0 +1,126 @@ + + +
+
+
+ + +
+ + +
+ + {#if isAndroid} +
+

Filters

+
+ + + +
+
+ +
+
+ {/if} + +
+

Write Records

+
+ + +
+
+ + +
diff --git a/examples/api/src/views/Notifications.svelte b/examples/api/src/views/Notifications.svelte new file mode 100644 index 0000000..11273fd --- /dev/null +++ b/examples/api/src/views/Notifications.svelte @@ -0,0 +1,44 @@ + + + + diff --git a/examples/api/src/views/Opener.svelte b/examples/api/src/views/Opener.svelte new file mode 100644 index 0000000..e20e765 --- /dev/null +++ b/examples/api/src/views/Opener.svelte @@ -0,0 +1,68 @@ + + +
+
+ + + with + +
+ +
+ + + with + +
+ +
+ + +
+
diff --git a/examples/api/src/views/Positioner.svelte b/examples/api/src/views/Positioner.svelte new file mode 100644 index 0000000..1f79167 --- /dev/null +++ b/examples/api/src/views/Positioner.svelte @@ -0,0 +1,40 @@ + + +
+

+ The tray positions only resolve once the tray icon has been clicked, which + reports its location to the plugin. +

+
+ + + +
+
diff --git a/examples/api/src/views/Scanner.svelte b/examples/api/src/views/Scanner.svelte new file mode 100644 index 0000000..ea0609e --- /dev/null +++ b/examples/api/src/views/Scanner.svelte @@ -0,0 +1,163 @@ + + +
+
+
+ + +
+
+ +
+ +
+
+
+ +
+
+
+
+

Aim your camera at a QR code

+ +
+
+
+
+
+
+
+
+
+
+ + diff --git a/examples/api/src/views/Shell.svelte b/examples/api/src/views/Shell.svelte new file mode 100644 index 0000000..faaa8c4 --- /dev/null +++ b/examples/api/src/views/Shell.svelte @@ -0,0 +1,100 @@ + + +
+
+ Script: + +
+
+ Encoding: + +
+
+ Working directory: + +
+
+ Arguments: + +
+
+ + +
+ {#if child} +
+ + + {/if} +
diff --git a/examples/api/src/views/Shortcuts.svelte b/examples/api/src/views/Shortcuts.svelte new file mode 100644 index 0000000..41d0271 --- /dev/null +++ b/examples/api/src/views/Shortcuts.svelte @@ -0,0 +1,72 @@ + + +
+ + +
+
+
+ {#each $shortcuts as savedShortcut} +
+ {savedShortcut} + +
+ {/each} + {#if $shortcuts.length > 1} +
+ + {/if} +
diff --git a/examples/api/src/views/Sql.svelte b/examples/api/src/views/Sql.svelte new file mode 100644 index 0000000..45e656e --- /dev/null +++ b/examples/api/src/views/Sql.svelte @@ -0,0 +1,68 @@ + + +
+
+ + +
+ + {#each todos as todo (todo.id)} +
+ toggle(todo)} + /> + {todo.title} + +
+ {/each} +
diff --git a/examples/api/src/views/Store.svelte b/examples/api/src/views/Store.svelte new file mode 100644 index 0000000..6ba9a9a --- /dev/null +++ b/examples/api/src/views/Store.svelte @@ -0,0 +1,114 @@ + + +
+
+
+ Key: + +
+ +
+ Value: + +
+ +
+ + + + + +
+
Store at {path} on disk
+
+ +
+

Store Values

+ {#each Object.entries(cache) as [k, v]} +
{k} = {v}
+ {/each} +
+
diff --git a/examples/api/src/views/Stronghold.svelte b/examples/api/src/views/Stronghold.svelte new file mode 100644 index 0000000..900b2bb --- /dev/null +++ b/examples/api/src/views/Stronghold.svelte @@ -0,0 +1,71 @@ + + +
+ {#if stronghold} +
+ + + +
+ {:else} +
+ + +
+ {/if} +
diff --git a/examples/api/src/views/Updater.svelte b/examples/api/src/views/Updater.svelte new file mode 100644 index 0000000..5fdc775 --- /dev/null +++ b/examples/api/src/views/Updater.svelte @@ -0,0 +1,98 @@ + + +
+ {#if !isChecking && !newUpdate} + + {:else if !isInstalling && newUpdate} + + {:else} +
+ {progress}% +
+
+ {/if} +
+ + diff --git a/examples/api/src/views/Upload.svelte b/examples/api/src/views/Upload.svelte new file mode 100644 index 0000000..0eeb13f --- /dev/null +++ b/examples/api/src/views/Upload.svelte @@ -0,0 +1,417 @@ + + +
+
+

File Download

+ +
+
+ + +
+ +
+ +
+ + +
+
+ + {#if downloadPath} +
+
+ File will be saved as: +
{downloadPath}
+
+
+ {/if} + + + + {#if downloadProgress} +
+
+ Progress: {downloadProgress.percentage}% + Speed: {Math.round(downloadProgress.transferSpeed / 1024)} KB/s +
+
+
+
+
+ {Math.round(downloadProgress.progressTotal / 1024)} KB / {Math.round(downloadProgress.total / 1024)} KB +
+
+ {/if} + + {#if downloadResult} +
+ +
+ {/if} +
+
+ +
+

File Upload

+ +
+
+ + +
+ +
+ +
+ + +
+
+ +
+ + +

Choose the HTTP method for the upload request

+
+ +
+
+ Upload Configuration: +
+ Method: {uploadMethod} | URL: {uploadUrl || 'Not set'} +
+
+
+ + + + {#if uploadProgress} +
+
+ Progress: {uploadProgress.percentage}% + Speed: {Math.round(uploadProgress.transferSpeed / 1024)} KB/s +
+
+
+
+
+ {Math.round(uploadProgress.progressTotal / 1024)} KB / {Math.round(uploadProgress.total / 1024)} KB +
+
+ {/if} + + {#if uploadResult} +
+ +
+ {/if} +
+
+
diff --git a/examples/api/src/views/WebRTC.svelte b/examples/api/src/views/WebRTC.svelte new file mode 100644 index 0000000..e0f9862 --- /dev/null +++ b/examples/api/src/views/WebRTC.svelte @@ -0,0 +1,56 @@ + + +
+
Not available for Linux
+ +
diff --git a/examples/api/src/views/WebSocket.svelte b/examples/api/src/views/WebSocket.svelte new file mode 100644 index 0000000..9a91f89 --- /dev/null +++ b/examples/api/src/views/WebSocket.svelte @@ -0,0 +1,55 @@ + + +
+ {#if ws} +
+ + + +
+ {:else} +
+ + +
+ {/if} +
diff --git a/examples/api/src/views/Welcome.svelte b/examples/api/src/views/Welcome.svelte new file mode 100644 index 0000000..5d42aaa --- /dev/null +++ b/examples/api/src/views/Welcome.svelte @@ -0,0 +1,47 @@ + + +

+ This is a demo of Tauri's API capabilities using the @tauri-apps/api package. It's used as the main validation app, serving as the test bed of our + development process. In the future, this app will be used on Tauri's integration + tests. +

+ +
+
+
+App name: {appName}
+App version: {version}
+Tauri version: {tauriVersion}
+
+
+
+ + +
diff --git a/examples/api/tsconfig.json b/examples/api/tsconfig.json new file mode 100644 index 0000000..c315e05 --- /dev/null +++ b/examples/api/tsconfig.json @@ -0,0 +1,34 @@ +{ + "compilerOptions": { + "moduleResolution": "bundler", + "target": "ESNext", + "module": "ESNext", + "noEmit": true, + /** + * svelte-preprocess cannot figure out whether you have + * a value or a type, so tell TypeScript to enforce using + * `import type` instead of `import` for Types. + */ + "verbatimModuleSyntax": true, + "isolatedModules": true, + "resolveJsonModule": true, + /** + * To have warnings / errors of the Svelte compiler at the + * correct position, enable source maps by default. + */ + "sourceMap": true, + "esModuleInterop": true, + "skipLibCheck": true, + /** + * Typecheck JS in `.svelte` and `.js` files by default. + * Disable this if you'd like to use dynamic types. + */ + "checkJs": true, + "types": ["vite/client", "svelte"] + }, + /** + * Use global.d.ts instead of compilerOptions.types + * to avoid limiting type declarations. + */ + "include": ["src/**/*.d.ts", "src/**/*.js", "src/**/*.ts", "src/**/*.svelte"] +} diff --git a/examples/api/unocss.config.js b/examples/api/unocss.config.js new file mode 100644 index 0000000..d2069a4 --- /dev/null +++ b/examples/api/unocss.config.js @@ -0,0 +1,100 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +import { defineConfig, presetIcons, presetUno, presetWebFonts } from 'unocss' +import extractorSvelte from '@unocss/extractor-svelte' + +export default defineConfig({ + theme: { + colors: { + primary: '#FFFFFF', + primaryLighter: '#e9ecef', + darkPrimary: '#1B1B1D', + darkPrimaryLighter: '#242526', + primaryText: '#1C1E21', + darkPrimaryText: '#E3E3E3', + secondaryText: '#858A91', + darkSecondaryText: '#C2C5CA', + accent: '#3578E5', + accentDark: '#306cce', + accentDarker: '#2d66c3', + accentDarkest: '#2554a0', + accentLight: '#538ce9', + accentLighter: '#72a1ed', + accentLightest: '#9abcf2', + accentText: '#FFFFFF', + darkAccent: '#67d6ed', + darkAccentDark: '#49cee9', + darkAccentDarker: '#39cae8', + darkAccentDarkest: '#19b5d5', + darkAccentLight: '#85def1', + darkAccentLighter: '#95e2f2', + darkAccentLightest: '#c2eff8', + darkAccentText: '#1C1E21', + code: '#d6d8da', + codeDark: '#282a2e', + hoverOverlay: 'rgba(0,0,0,.05)', + hoverOverlayDarker: 'rgba(0,0,0,.1)', + darkHoverOverlay: 'hsla(0,0%,100%,.05)', + darkHoverOverlayDarker: 'hsla(0,0%,100%,.1)' + } + }, + preflights: [ + { + getCSS: ({ theme }) => ` + ::-webkit-scrollbar-thumb { + background-color: ${theme.colors.accent}; + } + + .dark ::-webkit-scrollbar-thumb { + background-color: ${theme.colors.darkAccent}; + } + + code { + font-size: ${theme.fontSize.xs[0]}; + font-family: ${theme.fontFamily.mono}; + border-radius: ${theme.borderRadius['DEFAULT']}; + background-color: ${theme.colors.code}; + } + + .code-block { + font-family: ${theme.fontFamily.mono}; + font-size: ${theme.fontSize.sm[0]}; + } + + .dark code { + background-color: ${theme.colors.codeDark}; + } + ` + } + ], + shortcuts: { + btn: `select-none outline-none shadow-md p-2 rd-1 text-primaryText border-none font-400 dark:font-600 + bg-accent hover:bg-accentDarker active:bg-accentDarkest text-accentText + dark:bg-darkAccent dark:hover:bg-darkAccentDarker dark:active:bg-darkAccentDarkest dark:text-darkAccentText`, + nv: `decoration-none flex items-center relative p-2 rd-1 transition-all-125 ease + text-darkSecondaryText + hover:text-accent dark:hover:text-darkAccent + hover:bg-darkHoverOverlay hover:border-l-4`, + nv_selected: `nv bg-darkHoverOverlay text-accent dark:text-darkAccent border-l-4`, + note: `decoration-none flex-inline items-center relative p-2 rd-1 + border-l-4 border-accent dark:border-darkAccent + bg-accent/10 dark:bg-darkAccent/10`, + 'note-red': + 'note bg-red-700/10 dark:bg-red-700/10 after:bg-red-700 dark:after:bg-red-700', + input: + 'h-10 flex items-center outline-none border-none p-2 rd-1 shadow-md bg-primaryLighter dark:bg-darkPrimaryLighter text-primaryText dark:text-darkPrimaryText' + }, + presets: [ + presetUno(), + presetIcons(), + presetWebFonts({ + fonts: { + sans: 'Rubik', + mono: ['Fira Code', 'Fira Mono:400,700'] + } + }) + ], + extractors: [extractorSvelte] +}) diff --git a/examples/api/updater-test/.gitignore b/examples/api/updater-test/.gitignore new file mode 100644 index 0000000..78bd45a --- /dev/null +++ b/examples/api/updater-test/.gitignore @@ -0,0 +1,5 @@ +* + +!.gitignore +!README.md +!updater.json diff --git a/examples/api/updater-test/README.md b/examples/api/updater-test/README.md new file mode 100644 index 0000000..2155130 --- /dev/null +++ b/examples/api/updater-test/README.md @@ -0,0 +1,10 @@ +To test the updater: + +1. Comment out `verify_signature` inside `plugins/updater/src/updater.rs` +2. Run `pnpm tauri build --debug` to build the bundles +3. Copy the bundle you want to test to this folder (`examples/api/updater-test/`) +4. Run `pnpm tauri dev` and open the Updater tab to check + +If the bundle you're testing doesn't exist in the `updater.json` yet, add it manually and welcome to open an PR + +> The `updater.json` and debug bundles will be served by `vite` diff --git a/examples/api/updater-test/updater.json b/examples/api/updater-test/updater.json new file mode 100644 index 0000000..84edf61 --- /dev/null +++ b/examples/api/updater-test/updater.json @@ -0,0 +1,11 @@ +{ + "version": "2.1.0", + "notes": "Test update!", + "pub_date": "2026-03-01T14:04:20+00:00", + "platforms": { + "windows-x86_64": { + "signature": "", + "url": "http://localhost:5173/updater-test/Tauri API_2.0.0_x64_en-US.msi" + } + } +} diff --git a/examples/api/vite.config.ts b/examples/api/vite.config.ts new file mode 100644 index 0000000..aab7232 --- /dev/null +++ b/examples/api/vite.config.ts @@ -0,0 +1,34 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +import { defineConfig } from 'vite' +import Unocss from 'unocss/vite' +import { svelte } from '@sveltejs/vite-plugin-svelte' +import process from 'process' + +const host = process.env.TAURI_DEV_HOST + +// https://vitejs.dev/config/ +export default defineConfig(async () => { + return { + plugins: [Unocss(), svelte()], + build: { + rollupOptions: { + output: { + entryFileNames: `assets/[name].js`, + chunkFileNames: `assets/[name].js`, + assetFileNames: `assets/[name].[ext]` + } + } + }, + server: { + host: host || false, + port: 5173, + strictPort: true, + fs: { + allow: ['.'] + } + } + } +}) diff --git a/package.json b/package.json new file mode 100644 index 0000000..73d7977 --- /dev/null +++ b/package.json @@ -0,0 +1,33 @@ +{ + "name": "plugins-workspace", + "private": true, + "license": "MIT OR Apache-2.0", + "type": "module", + "scripts": { + "build": "pnpm run -r --parallel --filter !plugins-workspace --filter !\"./plugins/*/examples/**\" --filter !\"./examples/*\" build", + "lint": "eslint .", + "format": "prettier --write .", + "format:check": "prettier --check .", + "ts:check": "pnpm run -r ts:check", + "test:api-e2e": "pnpm run --filter \"api-e2e\" e2e", + "test:api-e2e:android": "pnpm run --filter \"api-e2e\" e2e:android", + "test:api-e2e:ios": "pnpm run --filter \"api-e2e\" e2e:ios", + "example:api:dev": "pnpm run --filter \"api\" tauri dev" + }, + "devDependencies": { + "@eslint/js": "10.0.1", + "@rollup/plugin-node-resolve": "16.0.3", + "@rollup/plugin-terser": "1.0.0", + "@rollup/plugin-typescript": "12.3.0", + "eslint": "10.4.0", + "eslint-config-prettier": "10.1.8", + "eslint-plugin-security": "4.0.1", + "prettier": "3.8.3", + "rollup": "4.63.1", + "tslib": "2.8.1", + "typescript": "6.0.3", + "typescript-eslint": "8.70.0" + }, + "minimumReleaseAge": 4320, + "packageManager": "pnpm@12.3.4" +} diff --git a/packages/api-e2e/README.md b/packages/api-e2e/README.md new file mode 100644 index 0000000..e39fc17 --- /dev/null +++ b/packages/api-e2e/README.md @@ -0,0 +1,251 @@ +# Plugins end-to-end tests + +WebdriverIO suite that exercises the JavaScript API of the plugins in this +repository against a **real** Tauri app — the [`examples/api`](../../examples/api) +validation app — rather than a mocked backend, on desktop (Linux, macOS, Windows) +and mobile (Android, iOS). Each plugin has its own spec file, shared by every +platform, and adding coverage for a new plugin API is normally just dropping in +one more spec. + +It mirrors the [`@tauri-apps/api` e2e suite](https://github.com/tauri-apps/tauri/tree/dev/packages/api-e2e) +in the core repository; the `@tauri-apps/api` modules themselves are covered there. + +## How it works + +- The example app is built with `withGlobalTauri: true`, so the `@tauri-apps/api` surface + is reachable on `window.__TAURI__` inside the webview, and every plugin's `api-iife.js` + registers its API next to it (`window.__TAURI__.fs`, `window.__TAURI__.clipboardManager`, + …, the package name without the `@tauri-apps/plugin-` prefix, camel-cased). +- On desktop, WebdriverIO drives the app through [`@crabnebula/tauri-driver`](https://www.npmjs.com/package/@crabnebula/tauri-driver), + which bridges the WebDriver protocol to each platform's webview: + - **macOS** — the CrabNebula Webdriver, which needs [`tauri-plugin-automation`](https://crates.io/crates/tauri-plugin-automation) + (registered in `examples/api` behind its off-by-default `automation` Cargo feature, which + the suite's build enables) and a locally-running `@crabnebula/test-runner-backend`, + authenticated with `CN_API_KEY`. + - **Linux** — `webkit2gtk-driver` (`WebKitWebDriver` on `PATH`). + - **Windows** — `msedgedriver.exe` on `PATH`. It hands the app the `--remote-debugging-port` + it attaches to through `WEBVIEW2_ADDITIONAL_BROWSER_ARGUMENTS`, which WebView2 ignores in an + elevated process ([wry#1782](https://github.com/tauri-apps/wry/issues/1782)), so the suite has + to run unelevated. +- On mobile, WebdriverIO drives the app through [Appium](https://appium.io) (started by + `@wdio/appium-service`; the drivers are plain devDependencies of this package, which Appium + picks up on its own): + - **Android** — the UiAutomator2 driver. The suite switches to the app's `WEBVIEW_*` + context, which chromedriver reaches through the WebView's debugging socket. Debug builds + turn that on (`setWebContentsDebuggingEnabled`), so the suite builds a debug APK. A + chromedriver matching the device's WebView is downloaded on demand (see `E2E_CHROMEDRIVER`). + - **iOS** — the XCUITest driver on a simulator, attaching to the WKWebView through the + WebKit remote inspector. Debug builds mark the webview `isInspectable`, so the suite + builds an unsigned debug simulator app. The inspector identifies an app by the + `application-identifier` entitlement that Xcode embeds when it code signs a simulator + build; an unsigned one has none and is listed as `process-` instead of + its bundle identifier, so the config has the driver match that name too + (`appium:additionalWebviewBundleIds`). The driver also starts with a script timeout of + 0, which the config raises to the 30s the other drivers default to, or every + `executeAsync` would time out at once. +- Specs never `eval` in the page. They pass a function to the [`tauri()`](test/helpers/index.ts) + helper, which serializes it and runs it via the driver's own (CSP-exempt) script injection, + handing it `window.__TAURI__` as the first argument and returning its JSON result. +- Each spec file gets its own session — a fresh `tauri-driver` (and therefore a fresh app + instance) on desktop, a fresh Appium session (which relaunches the app) on mobile — so + each plugin's suite runs in isolation. +- A small [fixture server](test/helpers/server.ts) is started for the whole run. It serves + the updater manifest the desktop e2e build points the updater at (see + [`tauri.e2e.conf.json`](tauri.e2e.conf.json)) and the upload/download fixtures. It listens + on the host's loopback; the iOS simulator shares that network stack, and on Android the + mobile config runs `adb reverse` so the same `127.0.0.1` URL works on the device. The + `http` specs use the echo server the example app itself spawns on port 3003, since that + is the only `http://` origin in the example's http scope. The fixture server also has a + WebSocket echo endpoint (`ws://127.0.0.1:3004/ws`) for the `websocket` specs. + +## What is (not) covered + +The example only registers each plugin on the platforms it supports, so the suites +split three ways: desktop-only plugins are skipped on mobile, mobile-only plugins are +skipped on desktop, and the rest run everywhere with the odd test gated. + +| Plugin | Coverage | On mobile | +| ------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------- | +| `autostart` | enable/disable/isEnabled, restoring the host's original state. | Skipped — desktop-only plugin. | +| `cli` | `getMatches` shape for an argument-less launch. | Skipped — desktop-only plugin. | +| `clipboard-manager` | text, HTML and image round-trips, `clear`, error paths. | Text only; HTML and images are unsupported there. | +| `deep-link` | `getCurrent` on a plain launch, `onOpenUrl` delivery, runtime `register`/`isRegistered`/`unregister` on Linux and Windows. | Only that runtime registration is unsupported. | +| `dialog` | Only that the API is registered — every dialog blocks on native UI the driver cannot operate. | Same. | +| `fs` | read/write/stat/copy/rename/remove/truncate, `FileHandle`, line iteration, watchers, scope. | All but the watchers (`fs:allow-watch` is desktop-only here). | +| `global-shortcut` | register/unregister/isRegistered/unregisterAll and error paths. Shortcuts cannot be triggered. | Skipped — desktop-only plugin. | +| `http` | `fetch` methods, headers, JSON/bytes/multipart bodies, the cookie jar, abort, scope, failures. | Same. | +| `log` | `attachLogger`/`attachConsole` for webview and Rust records, level filtering. | Same. | +| `notification` | Permission model and the `window.Notification` override. Sending is fire-and-forget and display is not observable. | Action types, pending/active lists after cancel/remove, channels, listeners — see below. | +| `opener` | Scope enforcement only — a successful open launches an external app the suite cannot close. | Same. | +| `os` | Every function but `version` (which has nothing to compare against), checked against what the app was built for and the host. | Same, minus `hostname`. | +| `positioner` | Screen and tray positions (with a tray rect handed in through `handleIconState`), `moveWindowConstrained`, the missing-tray error (WM dependent). | Skipped — desktop-only plugin. | +| `process` | Only that the API is registered — `exit`/`relaunch` terminate the app under test. | Same. | +| `shell` | `execute`, `spawn` with stdout/stderr/close events, stdin, `kill`, scope enforcement. | Scope only on iOS, which cannot spawn a process at all. | +| `sql` | SQLite `load`/`execute`/`select`/`close`, bound values, column types, app-registered migrations, error paths. | Same. | +| `store` | CRUD, persistence, auto-save, defaults/reset, reload, `getStore`, `LazyStore`, change events. | Same. | +| `stronghold` | Store records (persisted across reloads), wrong-password and unknown-client errors, vault secrets, BIP39/SLIP10 derivation and Ed25519 signing. | Same. | +| `updater` | `check` against the fixture manifest (update / 204 / older release). Installing is never exercised. | Skipped — desktop-only plugin. | +| `upload` | `download` and `upload` with progress, methods, headers and error paths. | Same (through `adb reverse` on Android). | +| `websocket` | Text/binary echo, ping/pong, handshake headers, listener removal, server and client close, connection and argument errors, against the fixture server. | Same (through `adb reverse` on Android). | +| `window-state` | `filename`, save/restore, and that a re-created window gets its saved size (WM dependent). | Skipped — desktop-only plugin. | +| mobile-only plugins | `barcode-scanner`, `biometric`, `geolocation`, `haptics` and `nfc` need hardware or native UI the driver cannot operate, so only their global API surface is asserted. | Only there. | + +The notification permission specs are desktop-only: a mobile app starts out ungranted and +`requestPermission` puts up a system dialog the session would then block on. The rest of the +notification API (action types, channels, the pending/active lists, listeners) only exists on +mobile, and is covered there without the permission. + +The [`plugins.spec.ts`](test/specs/plugins.spec.ts) spec additionally asserts that every +plugin the platform registers injects its global API with its documented members, and that +the _other_ platform's plugins are absent. That is what catches a plugin whose +`global_api_script_path` is missing from its `build.rs`, or a crate that is not target-gated +in the example's `Cargo.toml`. + +## Prerequisites + +```sh +# from the repo root +pnpm install +pnpm build # examples/api resolves the plugins' JS from their dist-js output +``` + +Platform driver dependencies: + +| Platform | Requirement | +| -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| macOS | `CN_API_KEY` env var (CrabNebula Cloud). The automation plugin and test-runner-backend are wired up already. | +| Linux | `webkit2gtk-driver` package (provides `WebKitWebDriver`). | +| Windows | `msedgedriver.exe` matching your Edge version, on `PATH`. Run the suite unelevated. | +| Android | The usual Tauri Android setup (`ANDROID_HOME`, `NDK_HOME`, a JDK), which also provides the `adb` the suite shells out to, plus a running emulator or a connected device with USB debugging. Network access the first time, for the chromedriver download. | +| iOS | macOS with Xcode and an iOS simulator runtime. The first session compiles WebDriverAgent (a few minutes). | + +## Running + +```sh +# desktop, from the repo root +pnpm test:api-e2e + +# or from this package +pnpm e2e + +# iterate without rebuilding the app every run +E2E_SKIP_BUILD=1 pnpm e2e + +# run a single plugin's spec +pnpm exec wdio run ./wdio.conf.ts --spec test/specs/fs.spec.ts + +# mobile (from the repo root; or `pnpm e2e:android` / `pnpm e2e:ios` from this package) +pnpm test:api-e2e:android +pnpm test:api-e2e:ios +``` + +The first desktop run builds the app with [`tauri.e2e.conf.json`](tauri.e2e.conf.json) as a +config override, which enables the example's `automation` feature and points the updater at +the fixture server; afterwards use `E2E_SKIP_BUILD=1` to reuse the existing binary. A binary +supplied through `E2E_SKIP_BUILD` or `E2E_APP_PATH` must have been built with that override: +the `updater` specs rely on its endpoint, and the CrabNebula Webdriver (always on macOS) +relies on the automation feature. + +The mobile configs ([`wdio.android.conf.ts`](wdio.android.conf.ts), [`wdio.ios.conf.ts`](wdio.ios.conf.ts), +sharing [`wdio.mobile.ts`](wdio.mobile.ts)) run `tauri android build --debug --apk` / +`tauri ios build --debug --target aarch64-sim --no-sign`, compiling only the Rust target the +device runs (the Android one is read from the connected device through `adb`). The Android +Studio and Xcode projects are committed under `examples/api/src-tauri/gen`, so they are only +initialized if that directory is missing. The app must be a **debug** build — release builds +have webview debugging off, and Appium cannot see the page. `E2E_SKIP_BUILD` and +`E2E_APP_PATH` (an `.apk` / simulator `.app`) work as on desktop. No config override is +passed there: both things it turns on belong to desktop-only plugins. + +## Environment variables + +| Variable | Purpose | +| ------------------- | ---------------------------------------------------------------------------------------- | +| `CN_API_KEY` | CrabNebula Cloud key. Required on macOS (and whenever `E2E_CN_WEBDRIVER=1`). | +| `E2E_SKIP_BUILD` | Skip the `tauri build` step and reuse the existing binary. | +| `E2E_APP_PATH` | Absolute path to a prebuilt app/binary to test (also implies skip-build). | +| `E2E_SKIP` | Comma-separated plugin names to skip, e.g. `E2E_SKIP=clipboard-manager,global-shortcut`. | +| `E2E_SKIP_WM` | Skip window-manager-dependent tests (the window-state size restore). | +| `E2E_SPEC_RETRIES` | Retry count for flaky spec files (default `0`). | +| `E2E_CN_WEBDRIVER` | Use the CrabNebula Webdriver on Linux/Windows too (instead of the native driver). | +| `E2E_NATIVE_DRIVER` | Path passed to `tauri-driver --native-driver` (e.g. a specific chromedriver). | +| `CARGO_TARGET_DIR` | Override the target dir the app binary is looked up in. | + +Mobile only: + +| Variable | Purpose | +| -------------------- | ---------------------------------------------------------------------------------------------------- | +| `E2E_ANDROID_TARGET` | Rust target for the APK (`aarch64`, `armv7`, `i686`, `x86_64`); default: the connected device's ABI. | +| `E2E_ANDROID_DEVICE` | `adb` serial of the device/emulator to use (`appium:udid`); default: the first connected one. | +| `E2E_ANDROID_AVD` | Name of an AVD for Appium to boot (`appium:avd`) instead of using an already-running emulator. | +| `E2E_CHROMEDRIVER` | chromedriver binary matching the device's WebView, instead of letting Appium download one. | +| `E2E_IOS_TARGET` | Rust target for the simulator app (`aarch64-sim` or `x86_64`); default: the host architecture. | +| `E2E_IOS_DEVICE` | Simulator UDID or name (as in `xcrun simctl list`); default: a booted iPhone, else the newest one. | +| `E2E_PLATFORM` | Set by the mobile configs for the spec workers (`android`/`ios`) — see `platform` in the helpers. | + +Appium's own log is written to `logs/wdio-appium.log` in this package. + +## Adding tests for a new plugin API + +1. **Add a spec.** Create `test/specs/.spec.ts` and use `describePlugin('', …)` + with the `tauri()` helper. It is picked up automatically by the `test/specs/**/*.spec.ts` + glob. Minimal example: + + ```ts + import { expect } from '@wdio/globals' + import { tauri, describePlugin } from '../helpers/index.js' + + describePlugin('os', () => { + it('reports the platform', async () => { + expect(await tauri((api) => api.os.platform())).toBe('linux') + }) + }) + ``` + + If the plugin is new to the example, register it in + [`examples/api/src-tauri/src/lib.rs`](../../examples/api/src-tauri/src/lib.rs), add it to + the example's `package.json`/`Cargo.toml`, add its `workspace:*` package to this package's + `devDependencies` and its namespace to the matching interface in + [`test/helpers/index.ts`](test/helpers/index.ts) — `CommonPluginApi`, `DesktopPluginApi` or + `MobilePluginApi` — plus the member list in `plugins.spec.ts`. + +2. **Grant permissions if needed.** If the API calls a command that the example does not + allow yet, add the permission to + [`examples/api/src-tauri/capabilities/base.json`](../../examples/api/src-tauri/capabilities/base.json) + (or `desktop.json`/`mobile.json` for platform-specific plugins). Each plugin documents its + permissions under `plugins//permissions/autogenerated/reference.md`. + +3. **Need a server?** Add a route to the [fixture server](test/helpers/server.ts) rather than + hitting the network; its URL is exported as `FIXTURE_SERVER_URL`. + +4. **Handle environment-sensitive cases.** Use `itWm` (instead of `it`) for assertions that + depend on a real window manager, and `eventually()` to poll for state that is applied + asynchronously. Branch on `platform` from the helpers (never `process.platform`, which is + the host running the emulator/simulator on mobile) for platform-specific behavior. + Files go under `scratchDir('')`, relative to `BaseDirectory.AppData`, which the + example's fs scope allows. + +5. **Gate what mobile does not have.** The same specs run on Android and iOS. Wrap tests of + desktop-only behavior — a command the mobile build does not expose, a mobile implementation + that answers "Unsupported on this platform", or a permission only the desktop capability + grants — in `itDesktop`, use `itOn('android', …)` / `itOn('ios', …)` for platform-specific + APIs, and pass `{ desktopOnly: true }` (or `{ mobileOnly: true }`) to `describePlugin` for + plugins the example does not register on the other side. Skipped tests show up as pending + rather than silently disappearing. + +### Rules for `tauri()` page functions + +The function you pass to `tauri()` runs **inside the webview**, serialized as a string: + +- It **cannot** close over anything from the spec module — pass every value it needs through + the trailing `tauri(fn, ...args)` arguments. +- It may only reference `api` (the `window.__TAURI__` object), those args, and browser + globals (`window`, `document`, `setTimeout`, `Promise`, …). +- Its return value must be JSON-serializable — return plain objects/primitives, not class + instances (call methods and return their results instead), and remember that `undefined` + values are dropped. +- Restore any app state you mutate (clipboard, registered shortcuts, window size, …); tests + within a spec file share the same app instance. +- For in-page waiting, wrap logic in a `Promise` with an explicit `setTimeout` rejection so a + failure surfaces as a message rather than an opaque driver timeout. + +Use `tauriError(fn, ...args)` to assert that a call rejects; it returns the rejection message. diff --git a/packages/api-e2e/package.json b/packages/api-e2e/package.json new file mode 100644 index 0000000..b8226b5 --- /dev/null +++ b/packages/api-e2e/package.json @@ -0,0 +1,62 @@ +{ + "name": "api-e2e", + "private": true, + "version": "0.0.0", + "description": "End-to-end test suite for the plugins' JavaScript APIs, driven against the examples/api app via WebdriverIO (tauri-driver on desktop, Appium on mobile).", + "license": "MIT OR Apache-2.0", + "type": "module", + "scripts": { + "e2e": "wdio run ./wdio.conf.ts", + "e2e:skip-build": "cross-env E2E_SKIP_BUILD=true wdio run ./wdio.conf.ts", + "e2e:android": "wdio run ./wdio.android.conf.ts", + "e2e:ios": "wdio run ./wdio.ios.conf.ts", + "ts:check": "tsc --noEmit" + }, + "devDependencies": { + "@crabnebula/tauri-driver": "^2.0.9", + "@crabnebula/test-runner-backend": "^0.3.0", + "@tauri-apps/api": "^2.11.0", + "@tauri-apps/plugin-autostart": "workspace:*", + "@tauri-apps/plugin-barcode-scanner": "workspace:*", + "@tauri-apps/plugin-biometric": "workspace:*", + "@tauri-apps/plugin-cli": "workspace:*", + "@tauri-apps/plugin-clipboard-manager": "workspace:*", + "@tauri-apps/plugin-deep-link": "workspace:*", + "@tauri-apps/plugin-dialog": "workspace:*", + "@tauri-apps/plugin-fs": "workspace:*", + "@tauri-apps/plugin-geolocation": "workspace:*", + "@tauri-apps/plugin-global-shortcut": "workspace:*", + "@tauri-apps/plugin-haptics": "workspace:*", + "@tauri-apps/plugin-http": "workspace:*", + "@tauri-apps/plugin-log": "workspace:*", + "@tauri-apps/plugin-nfc": "workspace:*", + "@tauri-apps/plugin-notification": "workspace:*", + "@tauri-apps/plugin-opener": "workspace:*", + "@tauri-apps/plugin-os": "workspace:*", + "@tauri-apps/plugin-positioner": "workspace:*", + "@tauri-apps/plugin-process": "workspace:*", + "@tauri-apps/plugin-shell": "workspace:*", + "@tauri-apps/plugin-sql": "workspace:*", + "@tauri-apps/plugin-store": "workspace:*", + "@tauri-apps/plugin-stronghold": "workspace:*", + "@tauri-apps/plugin-updater": "workspace:*", + "@tauri-apps/plugin-upload": "workspace:*", + "@tauri-apps/plugin-websocket": "workspace:*", + "@tauri-apps/plugin-window-state": "workspace:*", + "@types/node": "^22.10.0", + "@types/ws": "^8.18.1", + "@wdio/appium-service": "^9.31.9", + "@wdio/cli": "^9.31.9", + "@wdio/globals": "^9.31.3", + "@wdio/local-runner": "^9.31.9", + "@wdio/mocha-framework": "^9.31.9", + "@wdio/spec-reporter": "^9.31.2", + "appium": "^3.7.0", + "appium-uiautomator2-driver": "^8.7.0", + "appium-xcuitest-driver": "^12.12.4", + "cross-env": "^10.1.0", + "typescript": "6.0.3", + "webdriverio": "^9.31.9", + "ws": "^8.18.0" + } +} diff --git a/packages/api-e2e/tauri.e2e.conf.json b/packages/api-e2e/tauri.e2e.conf.json new file mode 100644 index 0000000..278624b --- /dev/null +++ b/packages/api-e2e/tauri.e2e.conf.json @@ -0,0 +1,13 @@ +{ + "$schema": "../../examples/api/node_modules/@tauri-apps/cli/config.schema.json", + "build": { + "features": ["automation"] + }, + "plugins": { + "updater": { + "endpoints": [ + "http://127.0.0.1:3004/updater/{{target}}/{{arch}}/{{current_version}}" + ] + } + } +} diff --git a/packages/api-e2e/test/helpers/index.ts b/packages/api-e2e/test/helpers/index.ts new file mode 100644 index 0000000..6da724c --- /dev/null +++ b/packages/api-e2e/test/helpers/index.ts @@ -0,0 +1,343 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +import { browser } from '@wdio/globals' +import type * as TauriApi from '@tauri-apps/api' +import type * as Autostart from '@tauri-apps/plugin-autostart' +import type * as BarcodeScanner from '@tauri-apps/plugin-barcode-scanner' +import type * as Biometric from '@tauri-apps/plugin-biometric' +import type * as Cli from '@tauri-apps/plugin-cli' +import type * as ClipboardManager from '@tauri-apps/plugin-clipboard-manager' +import type * as DeepLink from '@tauri-apps/plugin-deep-link' +import type * as Dialog from '@tauri-apps/plugin-dialog' +import type * as Fs from '@tauri-apps/plugin-fs' +import type * as Geolocation from '@tauri-apps/plugin-geolocation' +import type * as GlobalShortcut from '@tauri-apps/plugin-global-shortcut' +import type * as Haptics from '@tauri-apps/plugin-haptics' +import type * as Http from '@tauri-apps/plugin-http' +import type * as Log from '@tauri-apps/plugin-log' +import type * as Nfc from '@tauri-apps/plugin-nfc' +import type * as Notification from '@tauri-apps/plugin-notification' +import type * as Opener from '@tauri-apps/plugin-opener' +import type * as Os from '@tauri-apps/plugin-os' +import type * as Positioner from '@tauri-apps/plugin-positioner' +import type * as Process from '@tauri-apps/plugin-process' +import type * as Shell from '@tauri-apps/plugin-shell' +import type * as Sql from '@tauri-apps/plugin-sql' +import type * as Store from '@tauri-apps/plugin-store' +import type * as Stronghold from '@tauri-apps/plugin-stronghold' +import type * as Updater from '@tauri-apps/plugin-updater' +import type * as Upload from '@tauri-apps/plugin-upload' +import type * as WebSocket from '@tauri-apps/plugin-websocket' +import type * as WindowState from '@tauri-apps/plugin-window-state' + +/** + * The plugin APIs the example registers on every platform, keyed by the name + * each plugin's `api-iife.js` defines on `window.__TAURI__` (the package name + * without the `@tauri-apps/plugin-` prefix, camel-cased). `sql` and `websocket` + * only have a default export, so their global is that class itself. + */ +export interface CommonPluginApi { + clipboardManager: typeof ClipboardManager + deepLink: typeof DeepLink + dialog: typeof Dialog + fs: typeof Fs + http: typeof Http + log: typeof Log + notification: typeof Notification + opener: typeof Opener + os: typeof Os + process: typeof Process + shell: typeof Shell + sql: typeof Sql.default + store: typeof Store + stronghold: typeof Stronghold + upload: typeof Upload + websocket: typeof WebSocket.default +} + +/** The plugin APIs the example only registers on desktop (`#[cfg(desktop)]`). */ +export interface DesktopPluginApi { + autostart: typeof Autostart + cli: typeof Cli + globalShortcut: typeof GlobalShortcut + positioner: typeof Positioner + updater: typeof Updater + windowState: typeof WindowState +} + +/** The plugin APIs the example only registers on mobile (`#[cfg(mobile)]`). */ +export interface MobilePluginApi { + barcodeScanner: typeof BarcodeScanner + biometric: typeof Biometric + geolocation: typeof Geolocation + haptics: typeof Haptics + nfc: typeof Nfc +} + +/** + * Every plugin API the example can register. Only the platform-appropriate + * half is actually on `window.__TAURI__` at runtime — see `describePlugin`'s + * `desktopOnly`/`mobileOnly` options and `plugins.spec.ts`. + */ +export type PluginApi = CommonPluginApi & DesktopPluginApi & MobilePluginApi + +/** The `@tauri-apps/api` surface plus every plugin, as exposed on `window.__TAURI__`. */ +export type Api = typeof TauriApi & PluginApi + +/** OS the app under test runs on. */ +export type Platform = NodeJS.Platform | 'android' | 'ios' + +/** + * The platform of the app under test. The desktop suite drives an app on the + * host, so it is `process.platform`; the mobile configs (`wdio.android.conf.ts`, + * `wdio.ios.conf.ts`) drive an emulator/simulator and set `E2E_PLATFORM` for + * the spec workers instead. + */ +export const platform: Platform = + (process.env.E2E_PLATFORM as Platform | undefined) ?? process.platform + +export const isMobile = platform === 'android' || platform === 'ios' + +type PageOutcome = + | { ok: true; value: T } + | { ok: false; error: string; stack?: string } + +/** Thrown when the function passed to {@link tauri} rejects inside the webview. */ +export class TauriPageError extends Error { + pageStack?: string + constructor(message: string, pageStack?: string) { + super(message) + this.name = 'TauriPageError' + this.pageStack = pageStack + } +} + +/** + * Runs `fn` inside the app's webview with `window.__TAURI__` as its first argument + * and resolves with its (JSON-serializable) return value. + * + * `fn` is serialized with `Function.prototype.toString`, so it **cannot close over + * anything in the spec module** — every value it needs must be passed through `args`, + * and it may only reference `api`, those args, and browser globals (`window`, + * `document`, `setTimeout`, `Promise`, ...). + * + * @example + * const platform = await tauri((api) => api.os.platform()) + * const sum = await tauri((api, a, b) => a + b, 2, 3) + */ +export async function tauri( + fn: (api: Api, ...args: A) => R, + ...args: A +): Promise> { + // A string body (rather than passing `fn` directly) keeps this working across + // both the classic and bidi WebDriver protocols and avoids any in-page eval of + // our own — the driver injects this script itself, which is exempt from the + // app's CSP. `executeAsync` is used because promise support in `execute` is not + // uniform across the platform drivers tauri-driver and Appium proxy to. + // + // The outcome crosses the driver as a JSON string rather than an object so no + // driver gets to interpret its shape: the Selenium atoms that Appium runs + // scripts through on iOS turn any object with a numeric `length` property + // into an array. + const script = ` + var done = arguments[arguments.length - 1]; + var args = Array.prototype.slice.call(arguments, 0, arguments.length - 1); + var fn = (${fn.toString()}); + Promise.resolve() + .then(function () { return fn.apply(null, [window.__TAURI__].concat(args)); }) + .then( + function (value) { return { ok: true, value: value === undefined ? null : value }; }, + function (error) { + return { + ok: false, + error: error instanceof Error ? error.message : String(error), + stack: error instanceof Error ? error.stack : undefined + }; + } + ) + .then(function (outcome) { + try { + done(JSON.stringify(outcome)); + } catch (error) { + done(JSON.stringify({ ok: false, error: 'result is not JSON-serializable: ' + error })); + } + }); + ` + const raw: unknown = await browser.executeAsync(script, ...args) + const outcome = ( + typeof raw === 'string' ? JSON.parse(raw) : raw + ) as PageOutcome> | null + if (!outcome || typeof outcome !== 'object' || !('ok' in outcome)) { + throw new Error( + `tauri() bridge returned an unexpected value: ${JSON.stringify(outcome)}` + ) + } + if (!outcome.ok) { + throw new TauriPageError(outcome.error, outcome.stack) + } + return outcome.value +} + +/** + * Asserts that the page-side call rejects and returns the rejection message, + * so specs can assert on it. Throws if the call unexpectedly resolves. + */ +export async function tauriError( + fn: (api: Api, ...args: A) => unknown, + ...args: A +): Promise { + try { + await tauri(fn, ...args) + } catch (error) { + if (error instanceof TauriPageError) { + return error.message + } + // Some platform drivers (notably the Linux WebKitWebDriver) surface a + // page-side `invoke` rejection as a WebDriver-level error on the + // `execute/async` command instead of letting the in-page bridge report it + // as an `{ ok: false }` outcome. Fall back to that error's message so the + // backend rejection is still assertable. This is safe for error-path specs: + // they match the message against an expected pattern, so a genuine driver + // failure (whose message won't match) still fails the test. + if (error instanceof Error) { + return error.message + } + throw error + } + throw new Error('expected the API call to reject, but it resolved') +} + +/** + * Polls `check` until it returns without throwing or `timeout` elapses. + * Use for state that is applied asynchronously (window manager, file watcher, ...). + */ +export async function eventually( + check: () => T | Promise, + { + timeout = 10_000, + interval = 250 + }: { timeout?: number; interval?: number } = {} +): Promise { + const deadline = Date.now() + timeout + let lastError: unknown + for (;;) { + try { + return await check() + } catch (error) { + lastError = error + } + if (Date.now() > deadline) { + throw lastError instanceof Error + ? lastError + : new Error(String(lastError)) + } + await new Promise((resolve) => setTimeout(resolve, interval)) + } +} + +const skippedModules = (process.env.E2E_SKIP ?? '') + .split(',') + .map((entry) => entry.trim()) + .filter(Boolean) + +export interface DescribePluginOptions { + /** + * The example only registers the plugin on desktop (`autostart`, `cli`, + * `global-shortcut`, `positioner`, `updater`, `window-state`), so the whole + * suite is skipped on mobile. + */ + desktopOnly?: boolean + /** + * The example only registers the plugin on mobile (`barcode-scanner`, + * `biometric`, `geolocation`, `haptics`, `nfc`), so the whole suite is + * skipped on desktop. + */ + mobileOnly?: boolean +} + +/** + * `describe` wrapper keyed by plugin name (the `@tauri-apps/plugin-*` suffix). + * Any plugin listed in the comma-separated `E2E_SKIP` env var + * (e.g. `E2E_SKIP=clipboard-manager,global-shortcut`) is skipped. + */ +export function describePlugin(plugin: string, fn: () => void): void +export function describePlugin( + plugin: string, + options: DescribePluginOptions, + fn: () => void +): void +export function describePlugin( + plugin: string, + optionsOrFn: DescribePluginOptions | (() => void), + maybeFn?: () => void +): void { + const [options, fn] = + typeof optionsOrFn === 'function' + ? [{} as DescribePluginOptions, optionsOrFn] + : [optionsOrFn, maybeFn!] + const title = `@tauri-apps/plugin-${plugin}` + if ( + skippedModules.includes(plugin) + || (options.desktopOnly && isMobile) + || (options.mobileOnly && !isMobile) + ) { + describe.skip(title, fn) + } else { + describe(title, fn) + } +} + +/** + * `it` restricted to the given platform(s); skipped (as pending) elsewhere. + * Use for behavior that only exists on one OS. + */ +export function itOn( + platforms: Platform | Platform[], + title: string, + fn: () => void | Promise +): void { + const list = Array.isArray(platforms) ? platforms : [platforms] + if (list.includes(platform)) { + it(title, fn) + } else { + it.skip(title, fn) + } +} + +/** + * `it` for desktop-only behavior of a plugin that *is* registered on mobile: + * a command the mobile build does not expose (`#[cfg(desktop)]`), a mobile + * implementation that answers "Unsupported on this platform", or a permission + * the example only grants in its desktop capability. + */ +export function itDesktop(title: string, fn: () => void | Promise): void { + if (isMobile) { + it.skip(title, fn) + } else { + it(title, fn) + } +} + +/** + * `it` for assertions that depend on a real window manager (window size and + * position restore, ...). Skipped entirely when `E2E_SKIP_WM` is set (e.g. + * bare headless CI), and on mobile, which has no window manager. + */ +export function itWm(title: string, fn: () => void | Promise): void { + if (process.env.E2E_SKIP_WM || isMobile) { + it.skip(title, fn) + } else { + it(title, fn) + } +} + +/** + * A scratch directory the fs-backed specs may freely write to. It is relative + * to `BaseDirectory.AppData` (`$APPDATA`), which the example's fs scope allows + * recursively; `name` keeps each spec file's files apart. + */ +export function scratchDir(name: string): string { + return `e2e/${name}` +} diff --git a/packages/api-e2e/test/helpers/server.ts b/packages/api-e2e/test/helpers/server.ts new file mode 100644 index 0000000..204f741 --- /dev/null +++ b/packages/api-e2e/test/helpers/server.ts @@ -0,0 +1,155 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +import http from 'node:http' +import { WebSocketServer } from 'ws' + +/** + * Where the fixture server listens. The port is fixed because the updater + * endpoint is baked into the app at build time (see `tauri.e2e.conf.json`). + */ +export const FIXTURE_SERVER_PORT = 3004 +export const FIXTURE_SERVER_URL = `http://127.0.0.1:${FIXTURE_SERVER_PORT}` + +/** Version the `/updater` endpoint advertises by default. */ +export const UPDATER_FIXTURE_VERSION = '2.1.0' +export const UPDATER_FIXTURE_NOTES = 'Test update from the e2e fixture server' +/** `{{target}}` values with special behavior on the `/updater` endpoint. */ +export const UPDATER_TARGET_NO_UPDATE = 'e2e-no-update' +export const UPDATER_TARGET_OLDER = 'e2e-older' +export const UPDATER_FIXTURE_OLDER_VERSION = '1.0.0' + +/** Body served by `GET /download`. */ +export const DOWNLOAD_FIXTURE_BODY = + 'hello from the plugins e2e fixture server\n'.repeat(64) + +/** WebSocket endpoint of the fixture server (the `ws://` twin of {@link FIXTURE_SERVER_URL}). */ +export const WEBSOCKET_FIXTURE_URL = `ws://127.0.0.1:${FIXTURE_SERVER_PORT}/ws` +/** Text message that makes the `/ws` endpoint close the connection with {@link WEBSOCKET_CLOSE_CODE}. */ +export const WEBSOCKET_CLOSE_REQUEST = 'close-me' +export const WEBSOCKET_CLOSE_CODE = 4000 +export const WEBSOCKET_CLOSE_REASON = 'closed by the fixture server' + +export interface FixtureServer { + close(): void +} + +/** + * A tiny HTTP server the network-facing specs (updater, upload) talk to: + * + * - `GET /updater/{{target}}/{{arch}}/{{current_version}}` — an updater + * manifest in the dynamic format. Advertises {@link UPDATER_FIXTURE_VERSION}, + * or {@link UPDATER_FIXTURE_OLDER_VERSION} when the target is + * {@link UPDATER_TARGET_OLDER}, and replies `204 No Content` when it is + * {@link UPDATER_TARGET_NO_UPDATE}. + * - `GET /download` — {@link DOWNLOAD_FIXTURE_BODY} with a `Content-Length`. + * - `* /echo` — a JSON description of the request (`method`, `url`, `headers` + * and the utf-8 `body`). + * - `ws /ws` — a WebSocket echo endpoint: text and binary messages are sent + * back as-is, and {@link WEBSOCKET_CLOSE_REQUEST} makes the server close the + * connection with {@link WEBSOCKET_CLOSE_CODE}. On `/ws/headers` the server + * first sends the upgrade request's headers as a JSON text message. + */ +export function startFixtureServer(): Promise { + const server = http.createServer((req, res) => { + const chunks: Buffer[] = [] + req.on('data', (chunk: Buffer) => chunks.push(chunk)) + req.on('end', () => { + const body = Buffer.concat(chunks) + const url = new URL(req.url ?? '/', FIXTURE_SERVER_URL) + const [, route, ...rest] = url.pathname.split('/') + + if (route === 'updater' && req.method === 'GET') { + const [target] = rest + if (target === UPDATER_TARGET_NO_UPDATE) { + res.writeHead(204).end() + return + } + json(res, { + version: + target === UPDATER_TARGET_OLDER + ? UPDATER_FIXTURE_OLDER_VERSION + : UPDATER_FIXTURE_VERSION, + notes: UPDATER_FIXTURE_NOTES, + pub_date: '2026-03-01T14:04:20Z', + url: `${FIXTURE_SERVER_URL}/download`, + signature: '' + }) + return + } + + if (route === 'download' && req.method === 'GET') { + res + .writeHead(200, { + 'content-type': 'text/plain', + 'content-length': Buffer.byteLength(DOWNLOAD_FIXTURE_BODY) + }) + .end(DOWNLOAD_FIXTURE_BODY) + return + } + + if (route === 'echo') { + json(res, { + method: req.method, + url: req.url, + headers: req.headers, + body: body.toString('utf8') + }) + return + } + + res.writeHead(404).end() + }) + }) + + const wss = new WebSocketServer({ noServer: true }) + server.on('upgrade', (req, socket, head) => { + const { pathname } = new URL(req.url ?? '/', FIXTURE_SERVER_URL) + if (pathname !== '/ws' && pathname !== '/ws/headers') { + socket.destroy() + return + } + wss.handleUpgrade(req, socket, head, (ws) => { + if (pathname === '/ws/headers') { + ws.send(JSON.stringify(req.headers)) + } + ws.on('message', (data, isBinary) => { + if ( + !isBinary + && Buffer.isBuffer(data) + && data.toString('utf8') === WEBSOCKET_CLOSE_REQUEST + ) { + ws.close(WEBSOCKET_CLOSE_CODE, WEBSOCKET_CLOSE_REASON) + } else { + ws.send(data, { binary: isBinary }) + } + }) + }) + }) + + return new Promise((resolve, reject) => { + server.once('error', reject) + server.listen(FIXTURE_SERVER_PORT, '127.0.0.1', () => { + server.off('error', reject) + resolve({ + close: () => { + for (const client of wss.clients) client.terminate() + wss.close() + server.closeAllConnections() + server.close() + } + }) + }) + }) +} + +function json(res: http.ServerResponse, value: unknown) { + const payload = JSON.stringify(value) + res + .writeHead(200, { + 'content-type': 'application/json', + 'content-length': Buffer.byteLength(payload) + }) + .end(payload) +} diff --git a/packages/api-e2e/test/specs/app.spec.ts b/packages/api-e2e/test/specs/app.spec.ts new file mode 100644 index 0000000..a76604e --- /dev/null +++ b/packages/api-e2e/test/specs/app.spec.ts @@ -0,0 +1,71 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +import { expect } from '@wdio/globals' +import { tauri } from '../helpers/index.js' + +// The example app's own commands and events, which its "Communication" view +// drives; they double as a check that the app under test is the right one. + +describe('examples/api', () => { + it('is built from the example config', async () => { + const info = await tauri(async (api) => ({ + name: await api.app.getName(), + version: await api.app.getVersion(), + identifier: await api.app.getIdentifier(), + label: api.window.getCurrentWindow().label + })) + expect(info).toEqual({ + name: 'Tauri API', + version: '2.0.0', + identifier: 'com.tauri.api', + label: 'main' + }) + }) + + it('perform_request returns the backend response', async () => { + const response = await tauri((api) => + api.core.invoke('perform_request', { + endpoint: 'dummy endpoint arg', + body: { id: 5, name: 'test' } + }) + ) + expect(response).toBe('message response') + }) + + it('log_operation accepts an optional payload', async () => { + await tauri(async (api) => { + await api.core.invoke('log_operation', { event: 'tauri-click' }) + await api.core.invoke('log_operation', { + event: 'tauri-click', + payload: 'from e2e' + }) + return null + }) + }) + + it('js-event is answered with rust-event', async () => { + const reply = await tauri( + (api) => + new Promise<{ data: string }>((resolve, reject) => { + const webview = api.webview.getCurrentWebview() + webview + .listen<{ data: string }>('rust-event', (event) => + resolve(event.payload) + ) + .then((unlisten) => { + webview + .emit('js-event', 'this is the payload string') + .catch(reject) + setTimeout(() => { + unlisten() + reject(new Error('no rust-event reply received')) + }, 5000) + }) + .catch(reject) + }) + ) + expect(reply).toEqual({ data: 'something else' }) + }) +}) diff --git a/packages/api-e2e/test/specs/autostart.spec.ts b/packages/api-e2e/test/specs/autostart.spec.ts new file mode 100644 index 0000000..b45a0af --- /dev/null +++ b/packages/api-e2e/test/specs/autostart.spec.ts @@ -0,0 +1,58 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +import { expect } from '@wdio/globals' +import { tauri, describePlugin } from '../helpers/index.js' + +// Enabling autostart registers the app with the host for real (a Launch Agent +// on macOS, an XDG autostart entry on Linux, a `Run` registry value on +// Windows), so the spec puts back whatever state it found. + +describePlugin('autostart', { desktopOnly: true }, () => { + let initiallyEnabled = false + + before(async () => { + initiallyEnabled = await tauri((api) => api.autostart.isEnabled()) + }) + + after(async () => { + await tauri( + (api, enabled) => + enabled ? api.autostart.enable() : api.autostart.disable(), + initiallyEnabled + ) + }) + + it('enable and disable toggle isEnabled', async () => { + const states = await tauri(async (api) => { + await api.autostart.enable() + const enabled = await api.autostart.isEnabled() + await api.autostart.disable() + return { enabled, disabled: await api.autostart.isEnabled() } + }) + expect(states).toEqual({ enabled: true, disabled: false }) + }) + + it('enabling twice keeps it enabled', async () => { + const enabled = await tauri(async (api) => { + await api.autostart.enable() + await api.autostart.enable() + const enabled = await api.autostart.isEnabled() + await api.autostart.disable() + return enabled + }) + expect(enabled).toBe(true) + }) + + it('disable resolves when autostart is already disabled', async () => { + // Windows used to fail here, having no `Run` registry value to delete + const disabled = await tauri(async (api) => { + await api.autostart.enable() + await api.autostart.disable() + await api.autostart.disable() + return !(await api.autostart.isEnabled()) + }) + expect(disabled).toBe(true) + }) +}) diff --git a/packages/api-e2e/test/specs/cli.spec.ts b/packages/api-e2e/test/specs/cli.spec.ts new file mode 100644 index 0000000..e817bcc --- /dev/null +++ b/packages/api-e2e/test/specs/cli.spec.ts @@ -0,0 +1,33 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +import { expect } from '@wdio/globals' +import { tauri, describePlugin } from '../helpers/index.js' + +// The driver launches the app without arguments, so the matches reflect the +// CLI definition in the example's `tauri.conf.json` with nothing set. The +// example only registers the plugin on desktop, so the suite is skipped on +// mobile (which has no command line to begin with). + +describePlugin('cli', { desktopOnly: true }, () => { + it('getMatches reports every defined argument as unset', async () => { + const matches = await tauri((api) => api.cli.getMatches()) + expect(Object.keys(matches.args).sort()).toEqual([ + 'config', + 'theme', + 'verbose' + ]) + // flags resolve to `false`, arguments taking a value to `null` + expect(matches.args.verbose).toEqual({ value: false, occurrences: 0 }) + expect(matches.args.config).toEqual({ value: null, occurrences: 0 }) + expect(matches.args.theme).toEqual({ value: null, occurrences: 0 }) + }) + + it('getMatches reports no subcommand', async () => { + const subcommand = await tauri( + async (api) => (await api.cli.getMatches()).subcommand + ) + expect(subcommand).toBeNull() + }) +}) diff --git a/packages/api-e2e/test/specs/clipboard-manager.spec.ts b/packages/api-e2e/test/specs/clipboard-manager.spec.ts new file mode 100644 index 0000000..f5bf0e4 --- /dev/null +++ b/packages/api-e2e/test/specs/clipboard-manager.spec.ts @@ -0,0 +1,158 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +import { expect } from '@wdio/globals' +import { + tauri, + tauriError, + describePlugin, + itDesktop, + isMobile +} from '../helpers/index.js' + +// The mobile implementation only carries plain text: `write_html`, `write_image` +// and `read_image` answer "Unsupported on this platform" there. +describePlugin('clipboard-manager', () => { + it('writeText and readText round-trip', async () => { + const text = 'clipboard text from e2e — ✓' + expect( + await tauri(async (api, text) => { + await api.clipboardManager.writeText(text) + return api.clipboardManager.readText() + }, text) + ).toBe(text) + }) + + it('writeText replaces the previous contents', async () => { + expect( + await tauri(async (api) => { + await api.clipboardManager.writeText('first') + await api.clipboardManager.writeText('second') + return api.clipboardManager.readText() + }) + ).toBe('second') + }) + + itDesktop('writeHtml exposes the alt text as plain text', async () => { + expect( + await tauri(async (api) => { + await api.clipboardManager.writeHtml( + 'bold from e2e', + 'bold from e2e (alt)' + ) + return api.clipboardManager.readText() + }) + ).toBe('bold from e2e (alt)') + }) + + itDesktop('writeImage and readImage round-trip pixels', async () => { + // a 2x2 PNG: red, green / blue, white + const png = [ + 137, 80, 78, 71, 13, 10, 26, 10, 0, 0, 0, 13, 73, 72, 68, 82, 0, 0, 0, 2, + 0, 0, 0, 2, 8, 6, 0, 0, 0, 114, 182, 13, 36, 0, 0, 0, 18, 73, 68, 65, 84, + 120, 218, 99, 248, 207, 192, 240, 31, 12, 129, 52, 24, 0, 0, 73, 200, 9, + 247, 3, 217, 100, 241, 0, 0, 0, 0, 73, 69, 78, 68, 174, 66, 96, 130 + ] + const rgba = [ + 255, 0, 0, 255, 0, 255, 0, 255, 0, 0, 255, 255, 255, 255, 255, 255 + ] + const result = await tauri(async (api, png) => { + // encoded bytes are decoded by the plugin before hitting the clipboard + await api.clipboardManager.writeImage(new Uint8Array(png)) + const read = await api.clipboardManager.readImage() + const size = await read.size() + const bytes = Array.from(await read.rgba()) + await read.close() + return { size, bytes } + }, png) + expect(result.size).toEqual({ width: 2, height: 2 }) + expect(result.bytes).toEqual(rgba) + }) + + itDesktop( + 'writeImage accepts an Image built with the core image API', + async () => { + // `window.__TAURI__.image.Image` and the class the plugin's global script + // sees must be the same one for `transformImage`'s `instanceof` to hold. + const rgba = [ + 255, 0, 0, 255, 0, 255, 0, 255, 0, 0, 255, 255, 255, 255, 255, 255 + ] + const result = await tauri(async (api, rgba) => { + const image = await api.image.Image.new(rgba, 2, 2) + await api.clipboardManager.writeImage(image) + const read = await api.clipboardManager.readImage() + const size = await read.size() + const bytes = Array.from(await read.rgba()) + await image.close() + await read.close() + return { size, bytes } + }, rgba) + expect(result.size).toEqual({ width: 2, height: 2 }) + expect(result.bytes).toEqual(rgba) + } + ) + + itDesktop( + 'an Image read from the clipboard can be written back', + async () => { + const result = await tauri(async (api) => { + const read = await api.clipboardManager.readImage() + await api.clipboardManager.writeText('replaced by text') + // `Image` instances are passed by resource id + await api.clipboardManager.writeImage(read) + const again = await api.clipboardManager.readImage() + const size = await again.size() + await read.close() + await again.close() + return size + }) + expect(result).toEqual({ width: 2, height: 2 }) + } + ) + + itDesktop('writeImage accepts a 1x1 image', async () => { + // a 1x1 transparent PNG + const png = [ + 137, 80, 78, 71, 13, 10, 26, 10, 0, 0, 0, 13, 73, 72, 68, 82, 0, 0, 0, 1, + 0, 0, 0, 1, 8, 6, 0, 0, 0, 31, 21, 196, 137, 0, 0, 0, 11, 73, 68, 65, 84, + 120, 156, 99, 96, 0, 2, 0, 0, 5, 0, 1, 122, 94, 171, 63, 0, 0, 0, 0, 73, + 69, 78, 68, 174, 66, 96, 130 + ] + const size = await tauri(async (api, png) => { + await api.clipboardManager.writeImage(new Uint8Array(png)) + const read = await api.clipboardManager.readImage() + const size = await read.size() + await read.close() + return size + }, png) + expect(size).toEqual({ width: 1, height: 1 }) + }) + + it('readImage rejects when the clipboard holds text', async () => { + const message = await tauriError(async (api) => { + await api.clipboardManager.writeText('not an image') + await api.clipboardManager.readImage() + }) + expect(message).toMatch( + isMobile + ? /Unsupported on this platform/ + : /not available in the requested format/ + ) + }) + + it('clear empties the clipboard', async () => { + // reading an empty clipboard rejects on some platforms and yields an + // empty string on others + const text = await tauri(async (api) => { + await api.clipboardManager.writeText('to be cleared') + await api.clipboardManager.clear() + try { + return await api.clipboardManager.readText() + } catch { + return '' + } + }) + expect(text).toBe('') + }) +}) diff --git a/packages/api-e2e/test/specs/deep-link.spec.ts b/packages/api-e2e/test/specs/deep-link.spec.ts new file mode 100644 index 0000000..1deb711 --- /dev/null +++ b/packages/api-e2e/test/specs/deep-link.spec.ts @@ -0,0 +1,92 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +import { expect } from '@wdio/globals' +import { tauri, tauriError, describePlugin, itOn } from '../helpers/index.js' + +// The suite launches the app without a URL, so there is no current deep link, +// and it cannot open one through the OS either. `onOpenUrl` is exercised by +// emitting the event the plugin itself emits when the app is opened with one. +// +// Registering a scheme at runtime is only implemented on Linux (a `.desktop` +// handler plus `xdg-mime`) and Windows (the registry); macOS and mobile read +// the schemes from the bundle and report the runtime APIs as unsupported. + +/** A scheme no real app handles, so registering it cannot clobber anything. */ +const scheme = 'tauri-plugins-e2e' + +describePlugin('deep-link', () => { + it('getCurrent is null when the app was not opened with a URL', async () => { + expect(await tauri((api) => api.deepLink.getCurrent())).toBeNull() + }) + + it('onOpenUrl delivers the opened URLs until unlistened', async () => { + const received = await tauri( + async (api, urls) => { + const received: string[][] = [] + const unlisten = await api.deepLink.onOpenUrl((urls) => + received.push(urls) + ) + await api.event.emit('deep-link://new-url', urls) + await new Promise((resolve) => setTimeout(resolve, 500)) + unlisten() + await api.event.emit('deep-link://new-url', ['ignored://']) + await new Promise((resolve) => setTimeout(resolve, 500)) + return received + }, + [`${scheme}://open/path?query=1`] + ) + expect(received).toEqual([[`${scheme}://open/path?query=1`]]) + }) + + itOn( + ['linux', 'win32'], + 'register and unregister toggle the scheme handler', + async () => { + const result = await tauri(async (api, scheme) => { + await api.deepLink.register(scheme) + const registered = await api.deepLink.isRegistered(scheme) + await api.deepLink.unregister(scheme) + return { + registered, + afterUnregister: await api.deepLink.isRegistered(scheme) + } + }, scheme) + expect(result).toEqual({ registered: true, afterUnregister: false }) + } + ) + + itOn( + ['linux', 'win32'], + 'isRegistered is false for an unknown scheme', + async () => { + expect( + await tauri( + (api, scheme) => api.deepLink.isRegistered(scheme), + `${scheme}-unknown` + ) + ).toBe(false) + } + ) + + itOn( + ['darwin', 'android', 'ios'], + 'runtime registration is unsupported', + async () => { + for (const command of [ + 'register', + 'unregister', + 'isRegistered' + ] as const) { + const error = await tauriError( + // eslint-disable-next-line security/detect-object-injection + (api, command, scheme) => api.deepLink[command](scheme), + command, + scheme + ) + expect(error).toMatch(/unsupported platform/i) + } + } + ) +}) diff --git a/packages/api-e2e/test/specs/fs.spec.ts b/packages/api-e2e/test/specs/fs.spec.ts new file mode 100644 index 0000000..d590e03 --- /dev/null +++ b/packages/api-e2e/test/specs/fs.spec.ts @@ -0,0 +1,381 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +import { expect } from '@wdio/globals' +import { + tauri, + tauriError, + describePlugin, + itDesktop, + scratchDir +} from '../helpers/index.js' + +// Every path below is relative to `BaseDirectory.AppData`, which the example's +// fs scope allows recursively (`fs:scope-appdata-recursive`). +const dir = scratchDir('fs') + +describePlugin('fs', () => { + before(async () => { + await tauri(async (api, dir) => { + const baseDir = api.fs.BaseDirectory.AppData + if (await api.fs.exists(dir, { baseDir })) { + await api.fs.remove(dir, { baseDir, recursive: true }) + } + await api.fs.mkdir(dir, { baseDir, recursive: true }) + }, dir) + }) + + after(async () => { + await tauri(async (api, dir) => { + const baseDir = api.fs.BaseDirectory.AppData + if (await api.fs.exists(dir, { baseDir })) { + await api.fs.remove(dir, { baseDir, recursive: true }) + } + }, dir) + }) + + it('mkdir and exists report the scratch directory', async () => { + const result = await tauri(async (api, dir) => { + const baseDir = api.fs.BaseDirectory.AppData + return { + dir: await api.fs.exists(dir, { baseDir }), + missing: await api.fs.exists(`${dir}/does-not-exist`, { baseDir }) + } + }, dir) + expect(result).toEqual({ dir: true, missing: false }) + }) + + it('writeTextFile and readTextFile round-trip utf-8 text', async () => { + const text = 'Hello from the e2e suite — olá, 世界! 🎉\nsecond line\n' + const read = await tauri( + async (api, path, text) => { + const baseDir = api.fs.BaseDirectory.AppData + await api.fs.writeTextFile(path, text, { baseDir }) + return api.fs.readTextFile(path, { baseDir }) + }, + `${dir}/text.txt`, + text + ) + expect(read).toBe(text) + }) + + it('writeTextFile appends when asked to', async () => { + const read = await tauri(async (api, path) => { + const baseDir = api.fs.BaseDirectory.AppData + await api.fs.writeTextFile(path, 'first', { baseDir }) + await api.fs.writeTextFile(path, ' second', { baseDir, append: true }) + return api.fs.readTextFile(path, { baseDir }) + }, `${dir}/append.txt`) + expect(read).toBe('first second') + }) + + it('writeFile and readFile round-trip binary data', async () => { + const bytes = [0, 1, 2, 3, 250, 251, 252, 253, 254, 255] + const read = await tauri( + async (api, path, bytes) => { + const baseDir = api.fs.BaseDirectory.AppData + await api.fs.writeFile(path, new Uint8Array(bytes), { baseDir }) + return Array.from(await api.fs.readFile(path, { baseDir })) + }, + `${dir}/binary.bin`, + bytes + ) + expect(read).toEqual(bytes) + }) + + it('stat, lstat and size describe files and directories', async () => { + const result = await tauri(async (api, dir) => { + const baseDir = api.fs.BaseDirectory.AppData + const file = `${dir}/stat.txt` + await api.fs.writeTextFile(file, '0123456789', { baseDir }) + const fileStat = await api.fs.stat(file, { baseDir }) + const fileLstat = await api.fs.lstat(file, { baseDir }) + const dirStat = await api.fs.stat(dir, { baseDir }) + return { + file: { + isFile: fileStat.isFile, + isDirectory: fileStat.isDirectory, + isSymlink: fileStat.isSymlink, + size: fileStat.size, + hasMtime: fileStat.mtime instanceof Date + }, + lstatSize: fileLstat.size, + // `size` only takes absolute paths + size: await api.fs.size( + await api.path.join(await api.path.appDataDir(), file) + ), + dir: { isFile: dirStat.isFile, isDirectory: dirStat.isDirectory } + } + }, dir) + expect(result.file).toEqual({ + isFile: true, + isDirectory: false, + isSymlink: false, + size: 10, + hasMtime: true + }) + expect(result.lstatSize).toBe(10) + expect(result.size).toBe(10) + expect(result.dir).toEqual({ isFile: false, isDirectory: true }) + }) + + it('copyFile, rename and readDir', async () => { + const result = await tauri(async (api, dir) => { + const baseDir = api.fs.BaseDirectory.AppData + const sub = `${dir}/tree` + await api.fs.mkdir(`${sub}/nested`, { baseDir, recursive: true }) + await api.fs.writeTextFile(`${sub}/a.txt`, 'a', { baseDir }) + await api.fs.copyFile(`${sub}/a.txt`, `${sub}/b.txt`, { + fromPathBaseDir: baseDir, + toPathBaseDir: baseDir + }) + await api.fs.rename(`${sub}/b.txt`, `${sub}/c.txt`, { + oldPathBaseDir: baseDir, + newPathBaseDir: baseDir + }) + const entries = await api.fs.readDir(sub, { baseDir }) + return { + entries: entries + .map((e) => ({ + name: e.name, + isFile: e.isFile, + isDirectory: e.isDirectory + })) + .sort((x, y) => x.name.localeCompare(y.name)), + copied: await api.fs.readTextFile(`${sub}/c.txt`, { baseDir }), + renamedAway: await api.fs.exists(`${sub}/b.txt`, { baseDir }) + } + }, dir) + expect(result.entries).toEqual([ + { name: 'a.txt', isFile: true, isDirectory: false }, + { name: 'c.txt', isFile: true, isDirectory: false }, + { name: 'nested', isFile: false, isDirectory: true } + ]) + expect(result.copied).toBe('a') + expect(result.renamedAway).toBe(false) + }) + + it('remove deletes files and (recursively) directories', async () => { + const result = await tauri(async (api, dir) => { + const baseDir = api.fs.BaseDirectory.AppData + const sub = `${dir}/to-remove` + await api.fs.mkdir(`${sub}/nested`, { baseDir, recursive: true }) + await api.fs.writeTextFile(`${sub}/nested/file.txt`, 'x', { baseDir }) + await api.fs.remove(`${sub}/nested/file.txt`, { baseDir }) + const fileGone = !(await api.fs.exists(`${sub}/nested/file.txt`, { + baseDir + })) + await api.fs.writeTextFile(`${sub}/nested/other.txt`, 'x', { baseDir }) + let nonRecursiveError: string | null = null + try { + await api.fs.remove(sub, { baseDir }) + } catch (error) { + nonRecursiveError = String(error) + } + await api.fs.remove(sub, { baseDir, recursive: true }) + return { + fileGone, + nonRecursiveError, + dirGone: !(await api.fs.exists(sub, { baseDir })) + } + }, dir) + expect(result.fileGone).toBe(true) + // a non-empty directory cannot be removed without `recursive` + expect(result.nonRecursiveError).not.toBeNull() + expect(result.dirGone).toBe(true) + }) + + it('truncate shortens a file', async () => { + const result = await tauri(async (api, path) => { + const baseDir = api.fs.BaseDirectory.AppData + await api.fs.writeTextFile(path, '0123456789', { baseDir }) + await api.fs.truncate(path, 4, { baseDir }) + const shortened = await api.fs.readTextFile(path, { baseDir }) + await api.fs.truncate(path, undefined, { baseDir }) + return { + shortened, + emptied: await api.fs.readTextFile(path, { baseDir }) + } + }, `${dir}/truncate.txt`) + expect(result).toEqual({ shortened: '0123', emptied: '' }) + }) + + it('readTextFileLines iterates a file line by line', async () => { + const lines = await tauri(async (api, path) => { + const baseDir = api.fs.BaseDirectory.AppData + await api.fs.writeTextFile(path, 'one\ntwo\r\nthree', { baseDir }) + const result: string[] = [] + for await (const line of await api.fs.readTextFileLines(path, { + baseDir + })) { + result.push(line) + } + return result + }, `${dir}/lines.txt`) + expect(lines).toEqual(['one', 'two', 'three']) + }) + + it('FileHandle supports write, seek, read, stat and truncate', async () => { + const result = await tauri(async (api, path) => { + const baseDir = api.fs.BaseDirectory.AppData + const encoder = new TextEncoder() + const decoder = new TextDecoder() + + const created = await api.fs.create(path, { baseDir }) + const written = await created.write(encoder.encode('hello world')) + const statAfterWrite = await created.stat() + await created.close() + + const file = await api.fs.open(path, { baseDir, read: true, write: true }) + // seek past "hello " and read the rest + const position = await file.seek(6, api.fs.SeekMode.Start) + const buffer = new Uint8Array(32) + const read = await file.read(buffer) + const rest = decoder.decode(buffer.subarray(0, read ?? 0)) + // at EOF, read reports null + const atEof = await file.read(new Uint8Array(8)) + // relative and end-relative seeks + const fromEnd = await file.seek(-5, api.fs.SeekMode.End) + const relative = await file.seek(-1, api.fs.SeekMode.Current) + await file.truncate(5) + const statAfterTruncate = await file.stat() + await file.close() + + return { + written, + sizeAfterWrite: statAfterWrite.size, + position, + read, + rest, + atEof, + fromEnd, + relative, + sizeAfterTruncate: statAfterTruncate.size, + contents: await api.fs.readTextFile(path, { baseDir }) + } + }, `${dir}/handle.txt`) + expect(result).toEqual({ + written: 11, + sizeAfterWrite: 11, + position: 6, + read: 5, + rest: 'world', + atEof: null, + fromEnd: 6, + relative: 5, + sizeAfterTruncate: 5, + contents: 'hello' + }) + }) + + it('a closed FileHandle cannot be used again', async () => { + const message = await tauriError(async (api, path) => { + const baseDir = api.fs.BaseDirectory.AppData + const file = await api.fs.create(path, { baseDir }) + await file.close() + await file.stat() + }, `${dir}/closed.txt`) + expect(message).toMatch(/resource id \d+ is invalid/) + }) + + it('rejects paths outside the configured scope', async () => { + // `$HOME` itself is not in the example's fs scope (only the app dirs, + // `$DOWNLOAD` and `$RESOURCE` are). + const message = await tauriError(async (api) => + api.fs.readTextFile('e2e-forbidden.txt', { + baseDir: api.fs.BaseDirectory.Home + }) + ) + expect(message).toMatch(/forbidden path/) + }) + + it('rejects paths escaping the scope through `..`', async () => { + const message = await tauriError(async (api) => + api.fs.writeTextFile('../../e2e-escape.txt', 'nope', { + baseDir: api.fs.BaseDirectory.AppData + }) + ) + expect(message).toMatch(/cannot traverse directory|forbidden path/) + }) + + // The watch specs are desktop-only: `fs:allow-watch` is granted in the + // example's desktop capability only. + itDesktop( + 'watchImmediate reports changes in a watched directory', + async () => { + const result = await tauri(async (api, watched) => { + const baseDir = api.fs.BaseDirectory.AppData + await api.fs.mkdir(watched, { baseDir, recursive: true }) + const events: { kind: string; paths: string[] }[] = [] + const unwatch = await api.fs.watchImmediate( + watched, + (event) => { + events.push({ + kind: + typeof event.type === 'string' + ? event.type + : Object.keys(event.type)[0], + paths: event.paths + }) + }, + { baseDir, recursive: true } + ) + await api.fs.writeTextFile(`${watched}/touched.txt`, 'watched', { + baseDir + }) + // give the notifier a moment to deliver + const deadline = Date.now() + 10_000 + while ( + !events.some((e) => e.paths.some((p) => p.endsWith('touched.txt'))) + ) { + if (Date.now() > deadline) { + throw new Error( + `no watch event for touched.txt, got ${JSON.stringify(events)}` + ) + } + await new Promise((resolve) => setTimeout(resolve, 100)) + } + unwatch() + return events + }, `${dir}/watched`) + expect( + result.some((e) => e.paths.some((p) => p.endsWith('touched.txt'))) + ).toBe(true) + } + ) + + itDesktop('watch debounces and unwatch stops delivery', async () => { + const result = await tauri(async (api, watched) => { + const baseDir = api.fs.BaseDirectory.AppData + await api.fs.mkdir(watched, { baseDir, recursive: true }) + let count = 0 + const unwatch = await api.fs.watch( + watched, + () => { + count++ + }, + { baseDir, delayMs: 200 } + ) + await api.fs.writeTextFile(`${watched}/debounced.txt`, 'a', { + baseDir + }) + const deadline = Date.now() + 10_000 + while (count === 0) { + if (Date.now() > deadline) { + throw new Error('no debounced watch event received') + } + await new Promise((resolve) => setTimeout(resolve, 100)) + } + const afterFirst = count + unwatch() + await api.fs.writeTextFile(`${watched}/after-unwatch.txt`, 'b', { + baseDir + }) + await new Promise((resolve) => setTimeout(resolve, 1000)) + return { afterFirst, afterUnwatch: count } + }, `${dir}/debounced`) + expect(result.afterFirst).toBeGreaterThan(0) + expect(result.afterUnwatch).toBe(result.afterFirst) + }) +}) diff --git a/packages/api-e2e/test/specs/global-shortcut.spec.ts b/packages/api-e2e/test/specs/global-shortcut.spec.ts new file mode 100644 index 0000000..89aa300 --- /dev/null +++ b/packages/api-e2e/test/specs/global-shortcut.spec.ts @@ -0,0 +1,103 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +import { expect } from '@wdio/globals' +import { + tauri, + tauriError, + describePlugin, + platform +} from '../helpers/index.js' + +// Triggering a shortcut needs OS-level synthetic input the WebDriver session +// cannot produce, so the specs cover the registry (register / isRegistered / +// unregister / unregisterAll) and the error paths. The plugin is desktop-only, +// so the whole suite is skipped on mobile. + +describePlugin('global-shortcut', { desktopOnly: true }, () => { + afterEach(async () => { + await tauri((api) => api.globalShortcut.unregisterAll()) + }) + + it('register and unregister update isRegistered', async () => { + const result = await tauri(async (api) => { + const shortcut = 'CommandOrControl+Shift+F9' + const before = await api.globalShortcut.isRegistered(shortcut) + await api.globalShortcut.register(shortcut, () => {}) + const registered = await api.globalShortcut.isRegistered(shortcut) + await api.globalShortcut.unregister(shortcut) + const after = await api.globalShortcut.isRegistered(shortcut) + return { before, registered, after } + }) + expect(result).toEqual({ before: false, registered: true, after: false }) + }) + + it('register accepts a list of shortcuts and unregister a list too', async () => { + const result = await tauri(async (api) => { + const shortcuts = ['Alt+Shift+F7', 'Alt+Shift+F8'] + await api.globalShortcut.register(shortcuts, () => {}) + const registered = await Promise.all( + shortcuts.map((s) => api.globalShortcut.isRegistered(s)) + ) + await api.globalShortcut.unregister(shortcuts) + const after = await Promise.all( + shortcuts.map((s) => api.globalShortcut.isRegistered(s)) + ) + return { registered, after } + }) + expect(result.registered).toEqual([true, true]) + expect(result.after).toEqual([false, false]) + }) + + it('unregisterAll clears every registration', async () => { + const result = await tauri(async (api) => { + await api.globalShortcut.register('Alt+Shift+F5', () => {}) + await api.globalShortcut.register('Alt+Shift+F6', () => {}) + await api.globalShortcut.unregisterAll() + return [ + await api.globalShortcut.isRegistered('Alt+Shift+F5'), + await api.globalShortcut.isRegistered('Alt+Shift+F6') + ] + }) + expect(result).toEqual([false, false]) + }) + + it('shortcut names are normalized when checking registrations', async () => { + const result = await tauri(async (api) => { + await api.globalShortcut.register('CmdOrCtrl+Alt+F10', () => {}) + return { + aliased: await api.globalShortcut.isRegistered( + 'CommandOrControl+Alt+F10' + ), + reordered: await api.globalShortcut.isRegistered('Alt+CmdOrCtrl+F10') + } + }) + expect(result).toEqual({ aliased: true, reordered: true }) + }) + + it('registering the same shortcut twice rejects', async () => { + const message = await tauriError(async (api) => { + await api.globalShortcut.register('Alt+Shift+F11', () => {}) + try { + await api.globalShortcut.register('Alt+Shift+F11', () => {}) + } finally { + await api.globalShortcut.unregister('Alt+Shift+F11') + } + }) + // macOS does not check for duplicates itself: the OS rejects the second + // `RegisterEventHotKey` call and the error is the generic one + expect(message).toMatch( + platform === 'darwin' + ? /RegisterEventHotKey failed for F11/ + : /already registered/i + ) + }) + + it('rejects shortcuts that cannot be parsed', async () => { + const message = await tauriError((api) => + api.globalShortcut.register('NotAKey+Nope', () => {}) + ) + expect(message).toMatch(/NotAKey/) + }) +}) diff --git a/packages/api-e2e/test/specs/http.spec.ts b/packages/api-e2e/test/specs/http.spec.ts new file mode 100644 index 0000000..cd28320 --- /dev/null +++ b/packages/api-e2e/test/specs/http.spec.ts @@ -0,0 +1,149 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +import { expect } from '@wdio/globals' +import { tauri, tauriError, describePlugin } from '../helpers/index.js' + +// The example spawns an echo server on this port: it replies with the request +// body and the request headers, and sets a `session-token` cookie on requests +// that do not carry one. It is also the only `http://` origin in the example's +// http scope. +const echoServer = 'http://localhost:3003' + +describePlugin('http', () => { + it('fetch performs a GET and exposes status, url and headers', async () => { + const response = await tauri(async (api, url) => { + const response = await api.http.fetch(url, { + headers: { 'x-e2e-header': 'present' } + }) + return { + ok: response.ok, + status: response.status, + url: response.url, + // the echo server mirrors the request headers back + echoedHeader: response.headers.get('x-e2e-header'), + body: await response.text() + } + }, echoServer) + expect(response.ok).toBe(true) + expect(response.status).toBe(200) + expect(response.url).toBe(`${echoServer}/`) + expect(response.echoedHeader).toBe('present') + expect(response.body).toBe('') + }) + + it('fetch sends a JSON body and parses the echoed response', async () => { + const payload = { message: 'hello from e2e', nested: { list: [1, 2, 3] } } + const response = await tauri( + async (api, url, payload) => { + const response = await api.http.fetch(`${url}/json`, { + method: 'POST', + headers: { 'content-type': 'application/json' }, + body: JSON.stringify(payload) + }) + return { + status: response.status, + contentType: response.headers.get('content-type'), + body: (await response.json()) as unknown + } + }, + echoServer, + payload + ) + expect(response.status).toBe(200) + expect(response.contentType).toBe('application/json') + expect(response.body).toEqual(payload) + }) + + it('fetch sends binary bodies and reads them back as bytes', async () => { + const bytes = [0, 1, 2, 127, 128, 254, 255] + const echoed = await tauri( + async (api, url, bytes) => { + const response = await api.http.fetch(`${url}/bytes`, { + method: 'PUT', + body: new Uint8Array(bytes) + }) + return Array.from(new Uint8Array(await response.arrayBuffer())) + }, + echoServer, + bytes + ) + expect(echoed).toEqual(bytes) + }) + + it('fetch sends multipart form data', async () => { + const body = await tauri(async (api, url) => { + const form = new FormData() + form.append('foo', 'baz') + form.append('bar', 'qux') + const response = await api.http.fetch(`${url}/form`, { + method: 'POST', + body: form + }) + return { + contentType: response.headers.get('content-type'), + text: await response.text() + } + }, echoServer) + expect(body.contentType).toMatch(/^multipart\/form-data; boundary=/) + expect(body.text).toContain('name="foo"') + expect(body.text).toContain('baz') + expect(body.text).toContain('name="bar"') + expect(body.text).toContain('qux') + }) + + it('the cookie jar stores and replays cookies across requests', async () => { + const result = await tauri(async (api, url) => { + // The jar is persisted in the app data dir, so an earlier run (or the + // requests above) may already hold the cookie: the first request then + // replays it instead of being handed a new one. + const first = await api.http.fetch(`${url}/cookies`) + await first.text() + // Either way the jar attaches it to the next request, which the echo + // server mirrors back as a `cookie` header without setting a new one. + const second = await api.http.fetch(`${url}/cookies`) + await second.text() + return { + setCookie: first.headers.get('set-cookie'), + replayedFirst: first.headers.get('cookie'), + replayed: second.headers.get('cookie'), + setAgain: second.headers.get('set-cookie') + } + }, echoServer) + if (result.replayedFirst === null) { + expect(result.setCookie).toMatch(/^session-token=test-value/) + } else { + expect(result.replayedFirst).toContain('session-token=test-value') + } + expect(result.replayed).toContain('session-token=test-value') + expect(result.setAgain).toBeNull() + }) + + it('fetch can be aborted', async () => { + const message = await tauriError(async (api, url) => { + const controller = new AbortController() + controller.abort() + await api.http.fetch(`${url}/aborted`, { signal: controller.signal }) + }, echoServer) + expect(message).toMatch(/abort|cancel/i) + }) + + it('rejects URLs outside the configured scope', async () => { + const message = await tauriError((api) => + api.http.fetch('http://localhost:3999/not-in-scope') + ) + expect(message).toMatch(/url not allowed on the configured scope/) + }) + + it('network failures reject', async () => { + // Every in-scope origin is reachable, so route the request through a proxy + // nothing listens on to force a connection error. + const message = await tauriError( + (api, url) => + api.http.fetch(url, { proxy: { all: 'http://127.0.0.1:1' } }), + echoServer + ) + expect(message).toMatch(/error sending request/) + }) +}) diff --git a/packages/api-e2e/test/specs/log.spec.ts b/packages/api-e2e/test/specs/log.spec.ts new file mode 100644 index 0000000..74467f6 --- /dev/null +++ b/packages/api-e2e/test/specs/log.spec.ts @@ -0,0 +1,188 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +import { expect } from '@wdio/globals' +import { tauri, describePlugin, isMobile } from '../helpers/index.js' + +// The example registers the plugin with a `Webview` target, so records logged +// from the page (and from Rust) are forwarded back to `attachLogger`. +// +// The plugin's default format differs per platform: `[date][time][target][level] +// message` on desktop, and just `[target] message` on mobile, where the platform +// logger (logcat / os_log) already stamps the time and level. + +interface Record { + level: number + message: string +} + +type Level = 'error' | 'warn' | 'info' | 'debug' | 'trace' + +/** Logs `message` at `level` and resolves with the records `attachLogger` saw. */ +function logAndCollect(level: Level, message: string) { + return tauri( + (api, level, message) => + new Promise((resolve, reject) => { + const records: Record[] = [] + api.log + .attachLogger((record) => { + if (record.message.includes(message)) { + records.push(record) + } + }) + .then(async (detach) => { + // eslint-disable-next-line security/detect-object-injection + await api.log[level](message) + setTimeout(() => { + detach() + resolve(records) + }, 1000) + }) + .catch(reject) + }), + level, + message + ) +} + +describePlugin('log', () => { + it('attachLogger receives records logged from the webview', async () => { + const records = await logAndCollect('info', 'info record from e2e') + expect(records).toHaveLength(1) + expect(records[0].level).toBe(3) // LogLevel.Info + expect(records[0].message).toMatch( + isMobile + ? /\[webview[^\]]*\] info record from e2e$/ + : /\[webview[^\]]*\]\[INFO\] info record from e2e$/ + ) + }) + + it('records carry the level they were logged at', async () => { + const error = await logAndCollect('error', 'error record from e2e') + const warn = await logAndCollect('warn', 'warn record from e2e') + expect(error[0].level).toBe(5) // LogLevel.Error + expect(warn[0].level).toBe(4) // LogLevel.Warn + if (!isMobile) { + // the level is only part of the formatted message on desktop + expect(error[0].message).toContain('[ERROR]') + expect(warn[0].message).toContain('[WARN]') + } + }) + + it('records below the configured level are dropped', async () => { + // the example sets the level filter to Info + const debug = await logAndCollect('debug', 'debug record from e2e') + const trace = await logAndCollect('trace', 'trace record from e2e') + expect(debug).toHaveLength(0) + expect(trace).toHaveLength(0) + }) + + it('log options are accepted', async () => { + const records = await tauri( + (api, needle) => + new Promise((resolve, reject) => { + const records: Record[] = [] + api.log + .attachLogger((record) => { + if (record.message.includes(needle)) records.push(record) + }) + .then(async (detach) => { + await api.log.info(needle, { + file: 'e2e.spec.ts', + line: 42, + keyValues: { suite: 'plugins-e2e' } + }) + setTimeout(() => { + detach() + resolve(records) + }, 1000) + }) + .catch(reject) + }), + 'record with options from e2e' + ) + expect(records).toHaveLength(1) + expect(records[0].level).toBe(3) + }) + + it('detaching the logger stops delivery', async () => { + const count = await tauri( + (api, needle) => + new Promise((resolve, reject) => { + let count = 0 + api.log + .attachLogger((record) => { + if (record.message.includes(needle)) count++ + }) + .then(async (detach) => { + await api.log.info(needle) + await new Promise((r) => setTimeout(r, 500)) + detach() + await api.log.info(needle) + await new Promise((r) => setTimeout(r, 1000)) + resolve(count) + }) + .catch(reject) + }), + 'detached record from e2e' + ) + expect(count).toBe(1) + }) + + it('attachConsole forwards records to the console', async () => { + const forwarded = await tauri( + (api, needle) => + new Promise((resolve, reject) => { + const seen: string[] = [] + // Info records are forwarded to `console.info` + const original = console.info + console.info = (...args: unknown[]) => { + const text = args.map(String).join(' ') + if (text.includes(needle)) seen.push(text) + original.apply(console, args) + } + api.log + .attachConsole() + .then(async (detach) => { + await api.log.info(needle) + setTimeout(() => { + detach() + console.info = original + resolve(seen) + }, 1000) + }) + .catch(reject) + }), + 'console record from e2e' + ) + expect(forwarded.length).toBeGreaterThan(0) + }) + + it('records logged from Rust are forwarded to the webview too', async () => { + // the example's `log_operation` command logs its arguments at Info + const message = await tauri( + (api, needle) => + new Promise((resolve, reject) => { + api.log + .attachLogger((record) => { + if (record.message.includes(needle)) resolve(record.message) + }) + .then(() => + api.core.invoke('log_operation', { + event: 'tauri-click', + payload: needle + }) + ) + .catch(reject) + setTimeout(() => reject(new Error('record not received')), 5000) + }), + 'rust log from e2e' + ) + expect(message).toContain( + isMobile ? '[api_lib::cmd] tauri-click' : '[INFO] tauri-click' + ) + expect(message).toContain('rust log from e2e') + expect(message).not.toContain('[webview') + }) +}) diff --git a/packages/api-e2e/test/specs/notification.spec.ts b/packages/api-e2e/test/specs/notification.spec.ts new file mode 100644 index 0000000..c67256c --- /dev/null +++ b/packages/api-e2e/test/specs/notification.spec.ts @@ -0,0 +1,141 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +import { expect } from '@wdio/globals' +import { + tauri, + tauriError, + describePlugin, + itDesktop, + itOn +} from '../helpers/index.js' + +// Whether a notification actually shows up depends on the desktop session +// (a notification daemon on Linux, the app's registration on Windows and +// macOS), which the suite cannot observe, and sending is fire-and-forget, so +// only the permission model is covered on desktop. +// +// The permission specs are desktop-only: mobile starts out ungranted and +// `requestPermission` puts up a system dialog the session would then block on. +// +// Scheduling, action types, channels, the pending/active lists and the +// listeners are only implemented on mobile. None of them need the permission, +// so they are covered there on an app that has not been granted it. + +describePlugin('notification', () => { + itDesktop('permission is granted on desktop', async () => { + const result = await tauri(async (api) => ({ + granted: await api.notification.isPermissionGranted(), + requested: await api.notification.requestPermission() + })) + expect(result).toEqual({ granted: true, requested: 'granted' }) + }) + + itDesktop('the plugin overrides window.Notification', async () => { + const result = await tauri(async () => ({ + permission: window.Notification.permission, + requested: await window.Notification.requestPermission() + })) + expect(result).toEqual({ permission: 'granted', requested: 'granted' }) + }) + + itOn( + ['android', 'ios'], + 'action types register, and cancel/remove leave nothing pending or active', + async () => { + const result = await tauri(async (api) => { + await api.notification.registerActionTypes([ + { + id: 'e2e-actions', + actions: [ + { id: 'reply', title: 'Reply', input: true }, + { id: 'dismiss', title: 'Dismiss', destructive: true } + ] + } + ]) + await api.notification.cancel([424242]) + await api.notification.cancelAll() + await api.notification.removeActive([{ id: 424242 }]) + await api.notification.removeAllActive() + return { + pending: await api.notification.pending(), + active: await api.notification.active() + } + }) + expect(result).toEqual({ pending: [], active: [] }) + } + ) + + itOn('android', 'channels can be created, listed and removed', async () => { + const result = await tauri(async (api) => { + const { Importance, Visibility } = api.notification + await api.notification.createChannel({ + id: 'e2e-channel', + name: 'e2e channel', + description: 'created by the e2e suite', + importance: Importance.High, + visibility: Visibility.Public, + vibration: true + }) + const created = (await api.notification.channels()).find( + (channel) => channel.id === 'e2e-channel' + ) + await api.notification.removeChannel('e2e-channel') + const removed = (await api.notification.channels()).some( + (channel) => channel.id === 'e2e-channel' + ) + return { + created: created && { + name: created.name, + description: created.description, + importance: created.importance, + vibration: created.vibration + }, + removed + } + }) + expect(result).toEqual({ + created: { + name: 'e2e channel', + description: 'created by the e2e suite', + importance: 4, // Importance.High + vibration: true + }, + removed: false + }) + }) + + itOn('ios', 'channels are not implemented', async () => { + for (const call of ['create', 'remove', 'list'] as const) { + const error = await tauriError( + (api, call) => + call === 'create' + ? api.notification.createChannel({ id: 'e2e', name: 'e2e' }) + : call === 'remove' + ? api.notification.removeChannel('e2e') + : api.notification.channels(), + call + ) + expect(error).toMatch(/not implemented/) + } + }) + + itOn( + ['android', 'ios'], + 'onNotificationReceived and onAction listeners register and unregister', + async () => { + const result = await tauri(async (api) => { + const received = await api.notification.onNotificationReceived(() => {}) + const action = await api.notification.onAction(() => {}) + await received.unregister() + await action.unregister() + return { received: received.event, action: action.event } + }) + expect(result).toEqual({ + received: 'notification', + action: 'actionPerformed' + }) + } + ) +}) diff --git a/packages/api-e2e/test/specs/opener.spec.ts b/packages/api-e2e/test/specs/opener.spec.ts new file mode 100644 index 0000000..d5e1b73 --- /dev/null +++ b/packages/api-e2e/test/specs/opener.spec.ts @@ -0,0 +1,46 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +import { expect } from '@wdio/globals' +import { tauriError, describePlugin } from '../helpers/index.js' + +// A successful open launches an external application (browser, file manager) +// the suite cannot control or close, so only the scope enforcement is covered. +// The example allows `mailto:`, `tel:`, `http(s)://` URLs (opener:default), +// `https://` URLs specifically with `inAppBrowser`, and paths under `$APPDATA`. + +describePlugin('opener', () => { + it('openUrl rejects URL schemes outside the scope', async () => { + const message = await tauriError((api) => + api.opener.openUrl('ftp://example.com/file') + ) + expect(message).toMatch( + /Not allowed to open url ftp:\/\/example\.com\/file/ + ) + }) + + it('openUrl rejects an app that is not in the scope for the URL', async () => { + // `inAppBrowser` is only allowed for `https://` URLs + const message = await tauriError((api) => + api.opener.openUrl('http://example.com', 'inAppBrowser') + ) + expect(message).toMatch(/Not allowed to open url http:\/\/example\.com/) + }) + + it('openPath rejects paths outside the scope', async () => { + const message = await tauriError(async (api) => + api.opener.openPath(await api.path.join(await api.path.homeDir(), 'e2e')) + ) + expect(message).toMatch(/Not allowed to open path/) + }) + + it('revealItemInDir rejects paths that do not exist', async () => { + const message = await tauriError(async (api) => + api.opener.revealItemInDir( + await api.path.join(await api.path.appDataDir(), 'does-not-exist-e2e') + ) + ) + expect(message).toMatch(/os error 2/) + }) +}) diff --git a/packages/api-e2e/test/specs/os.spec.ts b/packages/api-e2e/test/specs/os.spec.ts new file mode 100644 index 0000000..8248475 --- /dev/null +++ b/packages/api-e2e/test/specs/os.spec.ts @@ -0,0 +1,109 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +import os from 'node:os' +import { expect } from '@wdio/globals' +import { + tauri, + describePlugin, + itDesktop, + platform, + isMobile +} from '../helpers/index.js' + +// Everything the plugin reports is baked in at compile time, so it describes +// the *app's* platform — the host for the desktop suite, but the emulator or +// simulator for the mobile ones, which is why nothing here compares against +// Node's view of the host unless the two are the same machine. + +const nodePlatformToTauri: Record = { + linux: 'linux', + win32: 'windows', + darwin: 'macos', + freebsd: 'freebsd', + openbsd: 'openbsd', + android: 'android', + ios: 'ios' +} + +const nodeArchToTauri: Record = { + x64: 'x86_64', + ia32: 'x86', + arm64: 'aarch64', + arm: 'arm', + riscv64: 'riscv64', + ppc64: 'powerpc64', + s390x: 's390x' +} + +// `Arch` in the plugin's guest-js. +const architectures = [ + 'x86', + 'x86_64', + 'arm', + 'aarch64', + 'mips', + 'mips64', + 'powerpc', + 'powerpc64', + 'riscv64', + 's390x', + 'sparc64' +] + +describePlugin('os', () => { + it('platform, type and family match the target', async () => { + const info = await tauri((api) => ({ + platform: api.os.platform(), + type: api.os.type(), + family: api.os.family() + })) + // eslint-disable-next-line security/detect-object-injection + expect(info.platform).toBe(nodePlatformToTauri[platform]) + // eslint-disable-next-line security/detect-object-injection + expect(info.type).toBe(nodePlatformToTauri[platform]) + expect(info.family).toBe(platform === 'win32' ? 'windows' : 'unix') + }) + + it('arch reports a known architecture', async () => { + const arch = await tauri((api) => api.os.arch()) + if (isMobile) { + // The device/simulator is not necessarily the host's architecture (see + // `E2E_ANDROID_TARGET` / `E2E_IOS_TARGET`). + expect(architectures).toContain(arch) + } else { + expect(arch).toBe(nodeArchToTauri[process.arch]) + } + }) + + it('eol and exeExtension match the platform conventions', async () => { + const info = await tauri((api) => ({ + eol: api.os.eol(), + exeExtension: api.os.exeExtension() + })) + expect(info.eol).toBe(platform === 'win32' ? '\r\n' : '\n') + expect(info.exeExtension).toBe(platform === 'win32' ? 'exe' : '') + }) + + it('locale is null or a language tag', async () => { + const locale = await tauri((api) => api.os.locale()) + // The plugin forwards the environment's POSIX locale as-is, so a host with + // no locale configured (CI runners default to `LANG=C.UTF-8`) reports the + // POSIX default instead of a language tag. + if (locale !== null && locale !== 'C' && locale !== 'POSIX') { + // e.g. `en-US` + const [language, ...subtags] = locale.split(/[-_]/) + expect(language).toMatch(/^[A-Za-z]{2,3}$/) + for (const subtag of subtags) { + expect(subtag).toMatch(/^[A-Za-z0-9]+$/) + } + } + }) + + itDesktop('hostname matches the host', async () => { + const hostname = await tauri((api) => api.os.hostname()) + // Windows can report the name in a different case than Node does. + expect(hostname!.toLowerCase()).toBe(os.hostname().toLowerCase()) + }) +}) diff --git a/packages/api-e2e/test/specs/plugins.spec.ts b/packages/api-e2e/test/specs/plugins.spec.ts new file mode 100644 index 0000000..7fdc009 --- /dev/null +++ b/packages/api-e2e/test/specs/plugins.spec.ts @@ -0,0 +1,234 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +import { expect } from '@wdio/globals' +import { + tauri, + isMobile, + type CommonPluginApi, + type DesktopPluginApi, + type MobilePluginApi +} from '../helpers/index.js' + +/** + * The members each plugin's `api-iife.js` is expected to define on + * `window.__TAURI__.`. This is the one place the suite covers the + * plugins whose commands cannot be driven from a WebDriver session (dialog + * blocks on native UI, process terminates the app, the mobile plugins need + * hardware or native UI), and it catches a plugin whose global API script is + * missing from its `build.rs`. + */ +type Surface = { [P in keyof T]: (keyof T[P])[] } + +/** Plugins the example registers on every platform. */ +const commonSurface: Surface = { + clipboardManager: [ + 'writeText', + 'readText', + 'writeHtml', + 'clear', + 'readImage', + 'writeImage' + ], + deepLink: [ + 'getCurrent', + 'register', + 'unregister', + 'isRegistered', + 'onOpenUrl' + ], + dialog: ['open', 'save', 'message', 'ask', 'confirm'], + fs: [ + 'BaseDirectory', + 'FileHandle', + 'SeekMode', + 'create', + 'open', + 'copyFile', + 'mkdir', + 'readDir', + 'readFile', + 'readTextFile', + 'readTextFileLines', + 'remove', + 'rename', + 'stat', + 'lstat', + 'truncate', + 'writeFile', + 'writeTextFile', + 'exists', + 'watch', + 'watchImmediate', + 'size' + ], + http: ['fetch'], + log: [ + 'LogLevel', + 'error', + 'warn', + 'info', + 'debug', + 'trace', + 'attachLogger', + 'attachConsole' + ], + notification: [ + 'isPermissionGranted', + 'requestPermission', + 'sendNotification', + 'registerActionTypes', + 'pending', + 'cancel', + 'cancelAll', + 'active', + 'removeActive', + 'removeAllActive', + 'createChannel', + 'removeChannel', + 'channels', + 'onNotificationReceived', + 'onAction' + ], + opener: ['openUrl', 'openPath', 'revealItemInDir'], + os: [ + 'eol', + 'platform', + 'family', + 'version', + 'type', + 'arch', + 'locale', + 'exeExtension', + 'hostname' + ], + process: ['exit', 'relaunch'], + shell: ['Command', 'Child', 'EventEmitter', 'open'], + // the `Database` class itself + sql: ['load', 'get'], + store: ['load', 'getStore', 'LazyStore', 'Store'], + stronghold: ['Location', 'Client', 'Store', 'Vault', 'Stronghold'], + upload: ['download', 'upload', 'HttpMethod'], + // the `WebSocket` class itself + websocket: ['connect'] +} + +/** Plugins the example only registers on desktop. */ +const desktopSurface: Surface = { + autostart: ['enable', 'disable', 'isEnabled'], + cli: ['getMatches'], + globalShortcut: ['register', 'unregister', 'unregisterAll', 'isRegistered'], + positioner: [ + 'Position', + 'moveWindow', + 'moveWindowConstrained', + 'handleIconState' + ], + updater: ['check', 'Update'], + windowState: [ + 'StateFlags', + 'restoreState', + 'restoreStateCurrent', + 'saveWindowState', + 'filename' + ] +} + +/** Plugins the example only registers on mobile. */ +const mobileSurface: Surface = { + barcodeScanner: [ + 'Format', + 'scan', + 'cancel', + 'checkPermissions', + 'requestPermissions', + 'openAppSettings' + ], + biometric: ['BiometryType', 'checkStatus', 'authenticate'], + geolocation: [ + 'watchPosition', + 'getCurrentPosition', + 'clearWatch', + 'checkPermissions', + 'requestPermissions' + ], + // `ImpactFeedbackStyle` and `NotificationFeedbackType` are type aliases, so + // they are not part of the runtime namespace. + haptics: [ + 'vibrate', + 'impactFeedback', + 'notificationFeedback', + 'selectionFeedback' + ], + nfc: [ + 'NFCTypeNameFormat', + 'TechKind', + 'RTD_TEXT', + 'RTD_URI', + 'record', + 'textRecord', + 'uriRecord', + 'scan', + 'write', + 'isAvailable' + ] +} + +const surface = { + ...commonSurface, + ...(isMobile ? mobileSurface : desktopSurface) +} as Record + +/** The other platform's plugins, which must *not* be in this build. */ +const foreign = Object.keys(isMobile ? desktopSurface : mobileSurface) + +describe('plugin globals', () => { + it('every plugin this platform registers exposes its API on window.__TAURI__', async () => { + const missing = await tauri( + (api, plugins) => + plugins.filter((plugin) => { + // eslint-disable-next-line security/detect-object-injection + const global = (api as unknown as Record)[plugin] + // a module namespace, or the class a default-export-only module (sql, websocket) defines + return typeof global !== 'object' && typeof global !== 'function' + }), + Object.keys(surface) + ) + expect(missing).toEqual([]) + }) + + it('the other platform’s plugins are not in the build', async () => { + // Their Rust crates are target-gated in the example's Cargo.toml, so their + // `global_api_script_path` is never injected either. + const present = await tauri( + (api, plugins) => + plugins.filter( + (plugin) => + // eslint-disable-next-line security/detect-object-injection + (api as unknown as Record)[plugin] !== undefined + ), + foreign + ) + expect(present).toEqual([]) + }) + + for (const [plugin, members] of Object.entries(surface)) { + it(`${plugin} exposes its documented members`, async () => { + const missing = await tauri( + (api, plugin, members) => { + const namespaces = api as unknown as Record< + string, + Record + > + // eslint-disable-next-line security/detect-object-injection + const namespace = namespaces[plugin] + return members.filter((member) => !(member in namespace)) + }, + plugin, + members + ) + expect(missing).toEqual([]) + }) + } +}) diff --git a/packages/api-e2e/test/specs/positioner.spec.ts b/packages/api-e2e/test/specs/positioner.spec.ts new file mode 100644 index 0000000..f2cfadf --- /dev/null +++ b/packages/api-e2e/test/specs/positioner.spec.ts @@ -0,0 +1,137 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +import { expect } from '@wdio/globals' +import { tauri, tauriError, describePlugin, itWm } from '../helpers/index.js' + +// Positions are computed from the window's current monitor and outer size, so +// the specs compute the expected physical position the same way and compare +// once the window manager has applied the move. The tray positions need the +// tray icon's rect, which normally comes from a click on the icon; the spec +// hands one in through `handleIconState`, as a JS tray event handler would. +// The moved window is not put back: each spec file gets a fresh app. + +interface Geometry { + monitor: { x: number; y: number; width: number; height: number } + window: { width: number; height: number } +} + +/** Moves the main window and reports where it ended up along with the geometry the position is derived from. */ +function moveAndMeasure(position: string, constrained = false) { + return tauri( + async (api, position, constrained) => { + const win = api.window.getCurrentWindow() + const { Position } = api.positioner + const value = Position[position as keyof typeof Position] + await (constrained + ? api.positioner.moveWindowConstrained(value) + : api.positioner.moveWindow(value)) + await new Promise((resolve) => setTimeout(resolve, 500)) + const monitor = await api.window.currentMonitor() + const size = await win.outerSize() + const outer = await win.outerPosition() + return { + monitor: { + x: monitor!.position.x, + y: monitor!.position.y, + width: monitor!.size.width, + height: monitor!.size.height + }, + window: { width: size.width, height: size.height }, + position: { x: outer.x, y: outer.y } + } + }, + position, + constrained + ) +} + +function center({ monitor, window }: Geometry) { + return { + x: monitor.x + Math.trunc(monitor.width / 2) - Math.trunc(window.width / 2), + y: + monitor.y + Math.trunc(monitor.height / 2) - Math.trunc(window.height / 2) + } +} + +describePlugin('positioner', { desktopOnly: true }, () => { + // must run before any tray rect is handed in + it('tray positions are rejected until the tray icon reported its rect', async () => { + const error = await tauriError((api) => + api.positioner.moveWindow(api.positioner.Position.TrayCenter) + ) + expect(error).toMatch(/Tray position not set/) + }) + + it('the Position enum matches the plugin', async () => { + const names = await tauri((api) => + Object.keys(api.positioner.Position).filter((key) => isNaN(Number(key))) + ) + expect(names).toEqual([ + 'TopLeft', + 'TopRight', + 'BottomLeft', + 'BottomRight', + 'TopCenter', + 'BottomCenter', + 'LeftCenter', + 'RightCenter', + 'Center', + 'TrayLeft', + 'TrayBottomLeft', + 'TrayRight', + 'TrayBottomRight', + 'TrayCenter', + 'TrayBottomCenter' + ]) + }) + + itWm('moveWindow centers the window on its monitor', async () => { + const result = await moveAndMeasure('Center') + expect(result.position).toEqual(center(result)) + }) + + itWm( + 'moveWindowConstrained behaves like moveWindow for screen positions', + async () => { + const result = await moveAndMeasure('Center', true) + expect(result.position).toEqual(center(result)) + } + ) + + itWm('tray positions are relative to the reported tray rect', async () => { + // a tray "icon" in the middle of the window's monitor, far from any edge, + // so neither the OS-specific flips nor the constraint kick in + const tray = await tauri(async (api) => { + const monitor = await api.window.currentMonitor() + const rect = { + x: monitor!.position.x + Math.trunc(monitor!.size.width / 2), + y: monitor!.position.y + Math.trunc(monitor!.size.height / 2), + width: 20, + height: 20 + } + await api.positioner.handleIconState({ + type: 'Click', + id: 'e2e', + position: new api.dpi.PhysicalPosition(rect.x, rect.y), + rect: { + position: new api.dpi.PhysicalPosition(rect.x, rect.y), + size: new api.dpi.PhysicalSize(rect.width, rect.height) + }, + button: 'Left', + buttonState: 'Down' + }) + return rect + }) + + const bottomLeft = await moveAndMeasure('TrayBottomLeft') + expect(bottomLeft.position).toEqual({ x: tray.x, y: tray.y }) + + const bottomCenter = await moveAndMeasure('TrayBottomCenter', true) + expect(bottomCenter.position).toEqual({ + x: tray.x + tray.width / 2 - Math.trunc(bottomCenter.window.width / 2), + y: tray.y + }) + }) +}) diff --git a/packages/api-e2e/test/specs/shell.spec.ts b/packages/api-e2e/test/specs/shell.spec.ts new file mode 100644 index 0000000..d43ce30 --- /dev/null +++ b/packages/api-e2e/test/specs/shell.spec.ts @@ -0,0 +1,238 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +import { expect } from '@wdio/globals' +import { + tauri, + tauriError, + describePlugin, + platform +} from '../helpers/index.js' + +// The example's shell scope allows `sh -c + + + + +
+

Welcome to Tauri!

+ +
+ +

Click on the Tauri logo to learn more about the framework

+ +
+ +
+
+

Requested intent:

+

+
+ +
+

initial intent:

+

+
+ +
+

updated intent by event:

+

+
+
+ + diff --git a/plugins/deep-link/examples/app/package.json b/plugins/deep-link/examples/app/package.json new file mode 100644 index 0000000..595af97 --- /dev/null +++ b/plugins/deep-link/examples/app/package.json @@ -0,0 +1,21 @@ +{ + "name": "deep-link-example", + "private": true, + "version": "2.2.13", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc && vite build", + "preview": "vite preview", + "tauri": "tauri" + }, + "dependencies": { + "@tauri-apps/api": "^2.11.0", + "@tauri-apps/plugin-deep-link": "workspace:*" + }, + "devDependencies": { + "@tauri-apps/cli": "2.11.4", + "typescript": "^6.0.3", + "vite": "^8.0.16" + } +} diff --git a/plugins/deep-link/examples/app/public/svelte.svg b/plugins/deep-link/examples/app/public/svelte.svg new file mode 100644 index 0000000..c5e0848 --- /dev/null +++ b/plugins/deep-link/examples/app/public/svelte.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/plugins/deep-link/examples/app/public/tauri.svg b/plugins/deep-link/examples/app/public/tauri.svg new file mode 100644 index 0000000..31b62c9 --- /dev/null +++ b/plugins/deep-link/examples/app/public/tauri.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/plugins/deep-link/examples/app/public/vite.svg b/plugins/deep-link/examples/app/public/vite.svg new file mode 100644 index 0000000..e7b8dfb --- /dev/null +++ b/plugins/deep-link/examples/app/public/vite.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/plugins/deep-link/examples/app/src-tauri/.gitignore b/plugins/deep-link/examples/app/src-tauri/.gitignore new file mode 100644 index 0000000..877b3f7 --- /dev/null +++ b/plugins/deep-link/examples/app/src-tauri/.gitignore @@ -0,0 +1,7 @@ +# Generated by Cargo +# will have compiled files and executables +/target/ + +/gen/schemas + +.cargo \ No newline at end of file diff --git a/plugins/deep-link/examples/app/src-tauri/Cargo.toml b/plugins/deep-link/examples/app/src-tauri/Cargo.toml new file mode 100644 index 0000000..1ebb5ac --- /dev/null +++ b/plugins/deep-link/examples/app/src-tauri/Cargo.toml @@ -0,0 +1,28 @@ +[package] +name = "deep-link-example" +version = "0.0.0" +description = "A Tauri App" +authors = ["you"] +license = "" +repository = "" +edition = "2021" +rust-version = "1.77.2" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[lib] +crate-type = ["staticlib", "cdylib", "rlib"] + +[build-dependencies] +tauri-build = { workspace = true } + +[dependencies] +serde = { workspace = true } +serde_json = { workspace = true } +tauri = { workspace = true, features = ["wry", "common-controls-v6", "x11"] } +tauri-plugin-deep-link = { path = "../../../" } +tauri-plugin-log = { path = "../../../../log" } +tauri-plugin-single-instance = { path = "../../../../single-instance", features = [ + "deep-link", +] } +log = "0.4" diff --git a/plugins/deep-link/examples/app/src-tauri/apple-app-site-association b/plugins/deep-link/examples/app/src-tauri/apple-app-site-association new file mode 100644 index 0000000..f94da1b --- /dev/null +++ b/plugins/deep-link/examples/app/src-tauri/apple-app-site-association @@ -0,0 +1,15 @@ +{ + "applinks": { + "details": [ + { + "appIDs": [ "DEVELOPMENT_TEAM_ID_HERE.com.tauri.deep-link-example" ], + "components": [ + { + "/": "/open/*", + "comment": "Matches any URL whose path starts with /open/" + } + ] + } + ] + } +} \ No newline at end of file diff --git a/plugins/deep-link/examples/app/src-tauri/assetlinks.json.txt b/plugins/deep-link/examples/app/src-tauri/assetlinks.json.txt new file mode 100644 index 0000000..0f09c9a --- /dev/null +++ b/plugins/deep-link/examples/app/src-tauri/assetlinks.json.txt @@ -0,0 +1 @@ +see https://fabianlars.de/.well-known/assetlinks.json \ No newline at end of file diff --git a/plugins/deep-link/examples/app/src-tauri/build.rs b/plugins/deep-link/examples/app/src-tauri/build.rs new file mode 100644 index 0000000..5ebf8d2 --- /dev/null +++ b/plugins/deep-link/examples/app/src-tauri/build.rs @@ -0,0 +1,7 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +fn main() { + tauri_build::build() +} diff --git a/plugins/deep-link/examples/app/src-tauri/capabilities/app.json b/plugins/deep-link/examples/app/src-tauri/capabilities/app.json new file mode 100644 index 0000000..a4bc7b4 --- /dev/null +++ b/plugins/deep-link/examples/app/src-tauri/capabilities/app.json @@ -0,0 +1,11 @@ +{ + "$schema": "../gen/schemas/desktop-schema.json", + "identifier": "run-app-base", + "description": "Base permissions to run the app", + "windows": ["main"], + "permissions": [ + "core:default", + "deep-link:allow-get-current", + "deep-link:default" + ] +} diff --git a/plugins/deep-link/examples/app/src-tauri/gen/android/.editorconfig b/plugins/deep-link/examples/app/src-tauri/gen/android/.editorconfig new file mode 100644 index 0000000..ebe51d3 --- /dev/null +++ b/plugins/deep-link/examples/app/src-tauri/gen/android/.editorconfig @@ -0,0 +1,12 @@ +# EditorConfig is awesome: https://EditorConfig.org + +# top-most EditorConfig file +root = true + +[*] +indent_style = space +indent_size = 2 +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = false +insert_final_newline = false \ No newline at end of file diff --git a/plugins/deep-link/examples/app/src-tauri/gen/android/.gitignore b/plugins/deep-link/examples/app/src-tauri/gen/android/.gitignore new file mode 100644 index 0000000..b248203 --- /dev/null +++ b/plugins/deep-link/examples/app/src-tauri/gen/android/.gitignore @@ -0,0 +1,19 @@ +*.iml +.gradle +/local.properties +/.idea/caches +/.idea/libraries +/.idea/modules.xml +/.idea/workspace.xml +/.idea/navEditor.xml +/.idea/assetWizardSettings.xml +.DS_Store +build +/captures +.externalNativeBuild +.cxx +local.properties +key.properties + +/.tauri +/tauri.settings.gradle \ No newline at end of file diff --git a/plugins/deep-link/examples/app/src-tauri/gen/android/app/.gitignore b/plugins/deep-link/examples/app/src-tauri/gen/android/app/.gitignore new file mode 100644 index 0000000..1efb55b --- /dev/null +++ b/plugins/deep-link/examples/app/src-tauri/gen/android/app/.gitignore @@ -0,0 +1,6 @@ +/src/main/java/com/tauri/deep_link_example/generated +/src/main/jniLibs/**/*.so +/src/main/assets/tauri.conf.json +/tauri.build.gradle.kts +/proguard-tauri.pro +/tauri.properties \ No newline at end of file diff --git a/plugins/deep-link/examples/app/src-tauri/gen/android/app/build.gradle.kts b/plugins/deep-link/examples/app/src-tauri/gen/android/app/build.gradle.kts new file mode 100644 index 0000000..13ec1ff --- /dev/null +++ b/plugins/deep-link/examples/app/src-tauri/gen/android/app/build.gradle.kts @@ -0,0 +1,70 @@ +import java.util.Properties + +plugins { + id("com.android.application") + id("org.jetbrains.kotlin.android") + id("rust") +} + +val tauriProperties = Properties().apply { + val propFile = file("tauri.properties") + if (propFile.exists()) { + propFile.inputStream().use { load(it) } + } +} + +android { + compileSdk = 36 + namespace = "com.tauri.deep_link_example" + defaultConfig { + manifestPlaceholders["usesCleartextTraffic"] = "false" + applicationId = "com.tauri.deep_link_example" + minSdk = 24 + targetSdk = 36 + versionCode = tauriProperties.getProperty("tauri.android.versionCode", "1").toInt() + versionName = tauriProperties.getProperty("tauri.android.versionName", "1.0") + } + buildTypes { + getByName("debug") { + manifestPlaceholders["usesCleartextTraffic"] = "true" + isDebuggable = true + isJniDebuggable = true + isMinifyEnabled = false + packaging { jniLibs.keepDebugSymbols.add("*/arm64-v8a/*.so") + jniLibs.keepDebugSymbols.add("*/armeabi-v7a/*.so") + jniLibs.keepDebugSymbols.add("*/x86/*.so") + jniLibs.keepDebugSymbols.add("*/x86_64/*.so") + } + } + getByName("release") { + isMinifyEnabled = true + proguardFiles( + *fileTree(".") { include("**/*.pro") } + .plus(getDefaultProguardFile("proguard-android-optimize.txt")) + .toList().toTypedArray() + ) + } + } + kotlinOptions { + jvmTarget = "1.8" + } + buildFeatures { + buildConfig = true + } +} + +rust { + rootDirRel = "../../../" +} + +dependencies { + implementation("androidx.webkit:webkit:1.14.0") + implementation("androidx.appcompat:appcompat:1.7.1") + implementation("androidx.activity:activity-ktx:1.10.1") + implementation("com.google.android.material:material:1.12.0") + testImplementation("junit:junit:4.13.2") + androidTestImplementation("androidx.test.ext:junit:1.1.4") + androidTestImplementation("androidx.test.espresso:espresso-core:3.5.0") +} + +apply(from = "tauri.build.gradle.kts") \ No newline at end of file diff --git a/plugins/deep-link/examples/app/src-tauri/gen/android/app/proguard-rules.pro b/plugins/deep-link/examples/app/src-tauri/gen/android/app/proguard-rules.pro new file mode 100644 index 0000000..481bb43 --- /dev/null +++ b/plugins/deep-link/examples/app/src-tauri/gen/android/app/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/plugins/deep-link/examples/app/src-tauri/gen/android/app/src/main/AndroidManifest.xml b/plugins/deep-link/examples/app/src-tauri/gen/android/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..591f3c6 --- /dev/null +++ b/plugins/deep-link/examples/app/src-tauri/gen/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,74 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/plugins/deep-link/examples/app/src-tauri/gen/android/app/src/main/java/com/tauri/deep_link_example/MainActivity.kt b/plugins/deep-link/examples/app/src-tauri/gen/android/app/src/main/java/com/tauri/deep_link_example/MainActivity.kt new file mode 100644 index 0000000..313161c --- /dev/null +++ b/plugins/deep-link/examples/app/src-tauri/gen/android/app/src/main/java/com/tauri/deep_link_example/MainActivity.kt @@ -0,0 +1,11 @@ +package com.tauri.deep_link_example + +import android.os.Bundle +import androidx.activity.enableEdgeToEdge + +class MainActivity : TauriActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + enableEdgeToEdge() + super.onCreate(savedInstanceState) + } +} diff --git a/plugins/deep-link/examples/app/src-tauri/gen/android/app/src/main/res/drawable-v24/ic_launcher_foreground.xml b/plugins/deep-link/examples/app/src-tauri/gen/android/app/src/main/res/drawable-v24/ic_launcher_foreground.xml new file mode 100644 index 0000000..2b068d1 --- /dev/null +++ b/plugins/deep-link/examples/app/src-tauri/gen/android/app/src/main/res/drawable-v24/ic_launcher_foreground.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/plugins/deep-link/examples/app/src-tauri/gen/android/app/src/main/res/drawable/ic_launcher_background.xml b/plugins/deep-link/examples/app/src-tauri/gen/android/app/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 0000000..07d5da9 --- /dev/null +++ b/plugins/deep-link/examples/app/src-tauri/gen/android/app/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/plugins/deep-link/examples/app/src-tauri/gen/android/app/src/main/res/layout/activity_main.xml b/plugins/deep-link/examples/app/src-tauri/gen/android/app/src/main/res/layout/activity_main.xml new file mode 100644 index 0000000..4fc2444 --- /dev/null +++ b/plugins/deep-link/examples/app/src-tauri/gen/android/app/src/main/res/layout/activity_main.xml @@ -0,0 +1,18 @@ + + + + + + \ No newline at end of file diff --git a/plugins/deep-link/examples/app/src-tauri/gen/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/plugins/deep-link/examples/app/src-tauri/gen/android/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..28f1aa119119336781390c2531c381499576ed59 GIT binary patch literal 3524 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 literal 0 HcmV?d00001 diff --git a/plugins/deep-link/examples/app/src-tauri/gen/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png b/plugins/deep-link/examples/app/src-tauri/gen/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png new file mode 100644 index 0000000000000000000000000000000000000000..28f1aa119119336781390c2531c381499576ed59 GIT binary patch literal 3524 zcmV;#4LkCQP)qWSpyDKsw z@SLEk+Ip+_KCZK+%Yb|B4_LOb-_x9os ziz^f-{8_*&)mxz0ZG+%6?sFnlnbNn1wdb0-omM9&+XoN|F}PKNz#t%%_y@&l5ID%3 z$eZC;t3ayqJw@|A%AdP(N6BU*t32FOz2hJ_eL)2XlFl3*KH-Bo9XAn?BZ`wM_&11} zAwb>)S4q5{0feG3Eendm6hxx}M9IucBv2qbca?4${o2nHDJ(*BfpE8316l?aJrnWI zS7${nBNYn4_Xthkfff)H@iKDrrswiycvU%Rh1tYeI=3@*>K0{*IE`l6dTJW%_`bmmZM;EYOSBGI=LJ_-bJ4sIYM>7vA%!QYNba3PGr%O%RF9l+E9k zu3Gzb$@0&c7aAWiV^zW39g44=e~_eK!-(iVmtN>LKJ26Xi`&L$VN zB{YIUq@xE051IXs$OV{aF+dP`Xm3YPRPTOh)}@h{dkLunzowuXitwt8Z2CCv^Be?5EyX zW2!d;Y9p&JdwG1jX{}lt+JkwaE-=v~azPIa7!c4izNZgILje5~Y>D45iP?T>OWe84 zkGdb8_C)x^#;D+{wXBbO{D?rj=}8`Tq7O@77PHf+i$>`$-Xa1HjRR3BHHC#ag3_LT z-uCG5iI!gX#^*hgHi(e3Iq)Q&P)ZR1P>K%i8+8Bf<=u`D3BvUN69kT=-0$euy^}e1 z<_CH6foK8{kNRi_G*fr~RP|j_N??D6r)8D&>}=@;Dki$1pI`4bQi)mO%)DOIB1Ed-6%Qrb2l>;8 z8S9&0qLg5#_st^knE;SudsAX0HY)=_AI@yyqdxjP!`ipsK&7NuhPEYWb!-VBydyZo zPe(aF9c{ePGN18koneUo-6~e~jvu+~>U52POsAet5<0e(+7c62cOGo8 z3w)H*pK{Fx!SMoSiiJ*{JJ0^`un~{}FHlOt4Df+LT=~Y;@8pge)V1QXjq8oIWeu&# zjQ8`V0k0Kv8_@!`n@3;8hf8GXrh)~T7hE~|AXTUR8jQr~fMhHP&G>;UhB>3KupB7X z7=quV6VQl34Vq))y0>{|`dhTD1Y4`9VGFR5!q;D%Q2E8DpK4Er^Gs3dO=PG{7+^+L zb!P8L=XGpVyO*A7H|4jQo!E3StJD(|46`SOv1li;Pll zoupu3P%8{27P+i7M9^bZ^+_mN5sC^0mK!HNe{0U+5QN9So{xCj&;*JGkygm=?FTA$ z>gwJ2fW$Xm&%ActK6vPpAf*BXE9*fkq;%Di`E`BI2S6J(;RJ;X5P&8KQ0WK=GKw_8 zVTnW22OZ2G0Lrl;yY#&&9p?4!9XJRkL#j12yFj+>uh?=W-;*z(@od5XHkt`|q!yjE zPrpZ*ENcqR+ib-3)8X{mGnr>FFbJ5|)uIlDIYy5gA2edjb8u~lH}}ffvQJhl^5kUa zXw+1VoBAEN(x5uflbO0;Qv0cMCr7+RD=8Cr&4WqANxiRcC|IQTkfz5}u`z3bEPxCI zNL{+dF|S8IEt!bx*uig%OmTIKf}97MgwKBE&CsbY&Zo|5%mOjTB_@EPjmdj{?0xQ& z$1?SXN$V9Q0&v&_^NJUu7LOklK3%P*P(v`$DhzCm#J{%m#@zGQYp?&_Ru11N2rS5u z&~PEp5e_SkK?|4>&xLcR4Zbq}+OZ_0B!>Kb+I+e27shIw3iE)Jl$F$Sq9laRS+GL4 zBUG(lc7)8E&~9e#=kdAYp9r5$s}-Crm{%W9rEhnaZFys3!EzLU-`k67dRhu{*r7|C zaVtWTTL3}JOG}86TmRY#=#x5_=yj|RY=oF9$z)btHN?~XcXgbPthd*(e^o3ka0&RCMm~ocR$ML@qqD^# z<|R&$LM3tQ_TtqKOuIP3H(WJm6Rk-Qw5rm{JhdJz?`Zxci3n{%!~7|y84NhwS;sT9 zjB%jjhC(sf>*1|7;?2E$8eDFkwy1`~)v{;iZGl!Hl$d#93YAP{W%BjO^RJG6YEkYK zy}yuVZ=Ugutel1%s8d9%tBOJUF(!&2%-!RY43Edv*=|st+EXXbl&@d;meekS<7{@3 zGn;v#O_&9M(BdVFrWsC{*K0wyBdPJ$6l*(^Bjfuq)ZwkP&XK8rqd&J%n3CW~6PZJ&QPZ|IC2Y2(Lr+_2+^&R^_19kF=L7ITkY zJsV!n8#QrnH(U2Xy@OMU->pRc);BJi#vo`OiL`lc)|~D+#Q-A58*}s`>y~w$SrStc zjP5MdbG!cbgXe!`U+RzH^$&Bf!x5Y|?yo{{h`r&6+hQy3;4PX%iSKDj@SeF3Z0TZmPj+?(e7<)R8;eC;=m~Ct7gZ*@E z7jBtS!f$N~;EKrCu4Fq#jhzlFs{NM=6pOPxICz*+Qh2pC`*fBOpyV$K_3i;dX~qz& zI(GfY<(s~}qy*ta+e(;{pp0pkajUJ+?6c-l5>dE#&Lmvh= zt8m!(AfCWShd+VV#C`H;1~fui2LJ=nn;>UwEHG3tOG0vdhvl302BiF@KdmfRkTsmi zYyt&y=f5y{m|9t0?pkDPbyyj*be1_1Pc z_X?3BJH|+nQJo0JS5>Kv7x*t!tPhw?Na2Dx&sJ{R{E0?4Y?3Wi)8-Yq?m$tU+P|}M zbI|a#@oIG?Q9T~?JUCcrvJ6cQ>O;~c7Zw%$7Pw$^(TXwGap1`W)$Q^YEqZBe)voPd z5uD@ImIG)8o&37EzIDT#dw14VtCe>hIj}o__T6T0G(QvdZ93{W6@uH?gU3ES#s1jHNv3;Z z;&|9w)=I(8?g00-K~Jr$aJf$%Plt_#<<5*f-}@RWZbY<7b8cxz_XU8vf|g!MDKJx{ zg6)ojVr>hNOHeOPrRbOI-itZ^b)PzU6zV0v&eG`nSt|L%4=e`5o;0_4axG|NcPtYP y!07X;)*G~$kC8g7tM8|2^YLA=@_!z8_V^!OU)8o@2#nAG0000QSgd_^dWEm#2zvb6^-@HdA8zdoW|L`5&dC6Pv^4I#TRMNHHdIk^#JBgGvgR#^Dg%|e zBXA(y%?RB)QKuPMA?kpX>e!fD3~^kHdV{Ru1nai_yyZkEMu<(^H8 zR@c4yS@HQTcq_-!oF0Uq#R2_;JV#Ui`fJ~Xvu{h7PZ=1B^2(GX5ee)Ds8dA$5Yavg z8DroXo-(uQiLbUat^3obZ;1+nkd*eF3R{~(2%GR#3fOX4KWN0 znq!PmwgRnM9QFWQ5Md7}6g(^HrNG%Z*6S3VH_SgZ8M0E^)lATr0UH(!mHQ_N@uhDM zDzPUS287&Fh{J*;V7nD?A`$BNNq>3iabIb5U8oxIofOA8hNYWwhi92bQPC__SK7s@`=LSOGbMIBmKolM(&7z{bFv0DdXg-!TU)` z0TI8+6>%qf^U0y*IlC^zt2fz-)yQc=o+Y2`8csrA=SpP9a6(E8h;QSSPB3VoRv)~6 zZzuhq5ja}FpG^jaDSt0}7aA^Y$(7)pAq>p?yAuDb*IMr}Y`9-j#Y?g_l4GL?M&0@8 z(3*oqIlCw1TJrIlz=sWs#3`c^9EBX*DxPX5f-NbIk3PR)*pH-mI65KX5#D>oYGwo- zw!LAzn)=aKL=CO`Wa!Do5A|Jeb%AYkY>Z%So1sXKXj^M-?-4X7W=x8^iR>#-v!Ft$ ztbokC-hO+pWkHWZYLX!_KF$!+>KcPY+=A`4hF+Or$}Y$@4}-#IHOaxYdAIs3V1EzU zosZBAqB9#_PG8-T(-(r;9S5-UFdWNOoeS31HwMoeVyscN8*iDG_{0mZFxD6ol9pa_ zdiMZk&OG!%&aU#JoFgUgX78_9J@~}U<7|_#bw(PcWr|w0xoIU5q1w2D3Y8D-7GZ?% zx@;KHEUTuGjfxWDADsKxz%TYxW>W5#JXklb@C5p!c8)tQWKCA`{T;7tWz%G+r z`2>sgw^v&7A|tCiFlI928U{D!`C9*7;e=~k;>e|gm z><`X=k&TV(uXvg%f9dY6VHh!t#=xpk_f4_SjOlAg2{uXrxI&`=A+H=nEj0H|$Gk`` z0>!Nc9fV$uzgcoI97U`8%D`|5F}x#uWBZ2MRR>r~QRLWr?wi&3tq*r_iOFeFy;CHi zU;x9S_`KeZco7(PFv*o|S81mzE zNwpop0^TT?jf??J*D(*rP>&oy;}JHNt0(;~Vcvozazj0Vx8ZDsJLoNB(xipeybz_s``6P>#jUzHa z(qJKjq^Vi14~M6{C}A!F8yXD)oJTvJT)2YxeX4C3LNK0nf41)ZGt;ik@oZYZo&@wD#GLJj*Bdbw~~MB07bUKfmrj|E_)dkq5#;hvSJP ziEW=pY$I#0+2Y!`wcIms(#YtECV??-Kj0}o3Xb8U3Tc_X^di2iFm^iDjXM57J3VmP z|6OCV?8SE}5m3AY@Wb7NPf6{QvGbTOW6w#FQG8U)Dz72^R(!f6f|{8&R1I;5GR3;l z`JjV&w=F4woKOQIBM*jBxTerzJa#IGJIrBN3u>XTEJ6hFjdKDTfkhiBkMTUzhlF2T@^HVUm=lP< z%=0wwcZOUmH#M!jQ2ZSYRUI;}BCCc)jhQf&ghFb98H<(jeEr2ofdggVQ)+QbQY*a% zol3|Ulx`Z9f0Ysl_*)Zl%q_8ZMyM4(VP#p4!(hY#%23Qo}+f$@BlrZ<{mq;K$$xV$C>>^~$*5`#cJ`)C{ zr&5Y6>-9owYyrJqK&1)8%<;nUwrP?}LT!X!XDObhW;}Q1ZkI<6BE&lCK9_WWwI{^W zynqWeGB`lVuBLjazOGJh84(96=H@$R6Q2+7713GjOp4bVWGogQvNl6Y6cahkEqWm3 z*>@LbtajM>0W!b_z6u-z-PFcWb6%?Y!|Jwj{a5K`3&Xe38g$r6Y}5aEzs9~T(9=3h)cC@}fuHX@k-4ve zWR#qi`xSk>O-Rce5Sb~xo7kf{>(alcK7ac}`;8{EU}#aQOfL=ZOnB@|dGmu$vc5ly z4g@nJS(t9auG;;yV4w}zLk0`!S?L#J>$aIU_DM<*Qu-yf&Tbb??Ia26OFUaQuA|jp z(+u%>6jR>Fv6gFZxE^38(>%{d6tj%13y>qhqX!R%P8~m`@&g*%imh2z`&|+2_PFRV zG=e78$%Q6BB6K_c`2uwmv|!`lVOJ!-QM3g_Vk#JGA)$Z* z-=NMtIcJ*etaYmGnhEU!Lq<6#i@?@X`@XYwFJm-YWVZ{((Lj*1I~dp66$^pk4{MWOxBuL9 zalEORm1RRlc6z^+>krZlYa}8JFAyU-Dhe_b5M8U6zv}*E_1mG-Co0;#9Q-imjT{v_ zot0^{TC0^EPN zX6CJ<ETEiFX%?bP zArV5BaM+Uc`(VDHe0#p#`r||QPLa>MyeJ3(Vm)3y?{dr1o!}Dt*d32Qzsxdm@@;Ad z^?E2gkFAwzCmPkR1Y49LXh2pie6c2Wjn2^Z30^9T)lVurZr6;y!9$@M8yR{>mh7<>_7yQsuLLg3VOf4 zso*yg3^_wb@O@GSKwFJbXL`S{L9D7c7Wlqwr*Ge`?J%9WFVqxvxE>U2?`}BEXcD5U z+3Sele*O$}kdg4PLc&WUdSp?LgeyAy9}0f)i2u6%G~53G-3(h6(Dps_00000NkvXX Hu0mjfi}+A- literal 0 HcmV?d00001 diff --git a/plugins/deep-link/examples/app/src-tauri/gen/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png b/plugins/deep-link/examples/app/src-tauri/gen/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png new file mode 100644 index 0000000000000000000000000000000000000000..13dd21476b5c7f69cdfb82969bce7150244c127f GIT binary patch literal 9081 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 literal 0 HcmV?d00001 diff --git a/plugins/deep-link/examples/app/src-tauri/gen/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png b/plugins/deep-link/examples/app/src-tauri/gen/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png new file mode 100644 index 0000000000000000000000000000000000000000..73e48dbfb7dc31ed1e951b77820af88b93fcc418 GIT binary patch literal 3377 zcmV-14bJk3P)QSgd_^dWEm#2zvb6^-@HdA8zdoW|L`5&dC6Pv^4I#TRMNHHdIk^#JBgGvgR#^Dg%|e zBXA(y%?RB)QKuPMA?kpX>e!fD3~^kHdV{Ru1nai_yyZkEMu<(^H8 zR@c4yS@HQTcq_-!oF0Uq#R2_;JV#Ui`fJ~Xvu{h7PZ=1B^2(GX5ee)Ds8dA$5Yavg z8DroXo-(uQiLbUat^3obZ;1+nkd*eF3R{~(2%GR#3fOX4KWN0 znq!PmwgRnM9QFWQ5Md7}6g(^HrNG%Z*6S3VH_SgZ8M0E^)lATr0UH(!mHQ_N@uhDM zDzPUS287&Fh{J*;V7nD?A`$BNNq>3iabIb5U8oxIofOA8hNYWwhi92bQPC__SK7s@`=LSOGbMIBmKolM(&7z{bFv0DdXg-!TU)` z0TI8+6>%qf^U0y*IlC^zt2fz-)yQc=o+Y2`8csrA=SpP9a6(E8h;QSSPB3VoRv)~6 zZzuhq5ja}FpG^jaDSt0}7aA^Y$(7)pAq>p?yAuDb*IMr}Y`9-j#Y?g_l4GL?M&0@8 z(3*oqIlCw1TJrIlz=sWs#3`c^9EBX*DxPX5f-NbIk3PR)*pH-mI65KX5#D>oYGwo- zw!LAzn)=aKL=CO`Wa!Do5A|Jeb%AYkY>Z%So1sXKXj^M-?-4X7W=x8^iR>#-v!Ft$ ztbokC-hO+pWkHWZYLX!_KF$!+>KcPY+=A`4hF+Or$}Y$@4}-#IHOaxYdAIs3V1EzU zosZBAqB9#_PG8-T(-(r;9S5-UFdWNOoeS31HwMoeVyscN8*iDG_{0mZFxD6ol9pa_ zdiMZk&OG!%&aU#JoFgUgX78_9J@~}U<7|_#bw(PcWr|w0xoIU5q1w2D3Y8D-7GZ?% zx@;KHEUTuGjfxWDADsKxz%TYxW>W5#JXklb@C5p!c8)tQWKCA`{T;7tWz%G+r z`2>sgw^v&7A|tCiFlI928U{D!`C9*7;e=~k;>e|gm z><`X=k&TV(uXvg%f9dY6VHh!t#=xpk_f4_SjOlAg2{uXrxI&`=A+H=nEj0H|$Gk`` z0>!Nc9fV$uzgcoI97U`8%D`|5F}x#uWBZ2MRR>r~QRLWr?wi&3tq*r_iOFeFy;CHi zU;x9S_`KeZco7(PFv*o|S81mzE zNwpop0^TT?jf??J*D(*rP>&oy;}JHNt0(;~Vcvozazj0Vx8ZDsJLoNB(xipeybz_s``6P>#jUzHa z(qJKjq^Vi14~M6{C}A!F8yXD)oJTvJT)2YxeX4C3LNK0nf41)ZGt;ik@oZYZo&@wD#GLJj*Bdbw~~MB07bUKfmrj|E_)dkq5#;hvSJP ziEW=pY$I#0+2Y!`wcIms(#YtECV??-Kj0}o3Xb8U3Tc_X^di2iFm^iDjXM57J3VmP z|6OCV?8SE}5m3AY@Wb7NPf6{QvGbTOW6w#FQG8U)Dz72^R(!f6f|{8&R1I;5GR3;l z`JjV&w=F4woKOQIBM*jBxTerzJa#IGJIrBN3u>XTEJ6hFjdKDTfkhiBkMTUzhlF2T@^HVUm=lP< z%=0wwcZOUmH#M!jQ2ZSYRUI;}BCCc)jhQf&ghFb98H<(jeEr2ofdggVQ)+QbQY*a% zol3|Ulx`Z9f0Ysl_*)Zl%q_8ZMyM4(VP#p4!(hY#%23Qo}+f$@BlrZ<{mq;K$$xV$C>>^~$*5`#cJ`)C{ zr&5Y6>-9owYyrJqK&1)8%<;nUwrP?}LT!X!XDObhW;}Q1ZkI<6BE&lCK9_WWwI{^W zynqWeGB`lVuBLjazOGJh84(96=H@$R6Q2+7713GjOp4bVWGogQvNl6Y6cahkEqWm3 z*>@LbtajM>0W!b_z6u-z-PFcWb6%?Y!|Jwj{a5K`3&Xe38g$r6Y}5aEzs9~T(9=3h)cC@}fuHX@k-4ve zWR#qi`xSk>O-Rce5Sb~xo7kf{>(alcK7ac}`;8{EU}#aQOfL=ZOnB@|dGmu$vc5ly z4g@nJS(t9auG;;yV4w}zLk0`!S?L#J>$aIU_DM<*Qu-yf&Tbb??Ia26OFUaQuA|jp z(+u%>6jR>Fv6gFZxE^38(>%{d6tj%13y>qhqX!R%P8~m`@&g*%imh2z`&|+2_PFRV zG=e78$%Q6BB6K_c`2uwmv|!`lVOJ!-QM3g_Vk#JGA)$Z* z-=NMtIcJ*etaYmGnhEU!Lq<6#i@?@X`@XYwFJm-YWVZ{((Lj*1I~dp66$^pk4{MWOxBuL9 zalEORm1RRlc6z^+>krZlYa}8JFAyU-Dhe_b5M8U6zv}*E_1mG-Co0;#9Q-imjT{v_ zot0^{TC0^EPN zX6CJ<ETEiFX%?bP zArV5BaM+Uc`(VDHe0#p#`r||QPLa>MyeJ3(Vm)3y?{dr1o!}Dt*d32Qzsxdm@@;Ad z^?E2gkFAwzCmPkR1Y49LXh2pie6c2Wjn2^Z30^9T)lVurZr6;y!9$@M8yR{>mh7<>_7yQsuLLg3VOf4 zso*yg3^_wb@O@GSKwFJbXL`S{L9D7c7Wlqwr*Ge`?J%9WFVqxvxE>U2?`}BEXcD5U z+3Sele*O$}kdg4PLc&WUdSp?LgeyAy9}0f)i2u6%G~53G-3(h6(Dps_00000NkvXX Hu0mjfi}+A- literal 0 HcmV?d00001 diff --git a/plugins/deep-link/examples/app/src-tauri/gen/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/plugins/deep-link/examples/app/src-tauri/gen/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..1d98044f13bdd3afc7cb426644ae118688c03c14 GIT binary patch literal 7971 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 Z{{b+Ide7&7`HKJm002ovPDHLkV1iT#V`TsU literal 0 HcmV?d00001 diff --git a/plugins/deep-link/examples/app/src-tauri/gen/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png b/plugins/deep-link/examples/app/src-tauri/gen/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png new file mode 100644 index 0000000000000000000000000000000000000000..a888b336b5bc84b42c0c266ce9b8ec5d9eafed0b GIT binary patch literal 18900 zcmbrFQ+p*$vxZk}n-gbZ+qP}nwkNjjH+C}N#GKf+ZLB0K*z^5{ebC)!bJOBVdl93iy{dcbYZ^1(U+oMSst^oj}u0~ljqZIi7SEhjvZkYhNv4erJt_|D*)_#TqIecaywIQ@Ik-50Wz#6?JEc_oZgr9Al zs0#qod<{mfv1}WByjtotM3T=kGY+d;3tLR7^4>lfB&Zdo(dLxAlW_KRVDjSSzq`aG zjWxh^t05Bzfv>+0plWDQ*6Fu)u=KY0R&l&hgIwj%1EW}eR22Xe`f<~66HsP zHFC=og{B!jZ7eQ19i44=6BryIX3nn8!1P`5n;vMy*)!%)5GfXaXkmU;|HbE7-T>Dm z3l0AspM?#)M15L?-Veak1v3gK@bZ0p1p2H7wH5mgt@P}K4jzh>ajqqCgs5<&uFD5g zvg?Jgg~EvL4V;(|tdY4Ke zWZhXuxpX;T>uTL-X|5I#>mry=-HUkqNs!uw^{We8-4#Dz2j1@$n3X+DP}&(3GHUC* zUB43*Oe8fx)*B5^h5(uCI5CW)ANQa_Fl<|}8dC$dik8s43^N2+_{Ln3n;x^{vcd61 zsG$>SiLbmvzG;$5NR|MIAU=RmS}lN68D&2Q_TFa9>lVmp^kVQV#l-kir|ndlL^*eP zuRvWbh#d^u$+$@&4wA*gUK2EaYOyo-C-o4WQNkL5Aj=vo1{ncq4J;_KVMOsB)m*mQ z7?LP4ZjyX2cprS^p%;ly;74y`(NUcb9W5}z5yPH?IIw*lVj5CAFBs|tJKNRpB-pRs z)>msYA)%@0O9qyn|5HV3=$rcPy+}$03KW}3k-&Au9npq6w#=k) zr^L#|E6S|^#W&q>-^pO#(}0)GQYKNZo^5mn>Lo0IAQYZz-JKhsGH?9btoc*A)_|A3 z?sLk9!AQ};7Oz;-#5)#(AT}!gu1Q(LYb#2>OS(D{s`vdeu2gOJqt^aMffTy7Rr7|k z16%3<84P1Nx+MjI`-_mCQoz|uccKIMs>ppMd^rw>y2o82;Q@dVk(K{ZkY%dX)ckwlsZMpgG7W5KS zv4NqitkV7xxyYUr$l!*chJm7v62AJFA-EM{HaS^n2KUdnlTVLOr!uRK@(6IY9=3y& z6Ir5moXBVXT1<`NYxDCQdcb0=g8pjP7gz;l0c!X<46T<}0_7TIRT#){ZsifDkjyEQvQo?mKl(B}!@pyE z^zgf0`hCXm${1bxB1zUPG7zI-l6qiC{=*3U&6*dEqt;2@=EBkeu0osk`vdGcO)w*~gKn zfterBkQ`prbXkL6p+q(HL-tFEO<4rfWxQk*3=q@ehzOgEN7b6-uSAQ5_m-GZKGd7T z7`@KRuFeNYeUxb;7{P5WX!-SkaVs_$$=z(OXA4|R9p%ZkZ}bKfw5n+qv_b(GBc-)( z%bG8&<1S4;;2n*aCoD{%SdoOOmcVs-TF2#sKNF1cgc_k%*$tLf`nK+)S1Q&kz&g~! zI!pRC-IYoILLq~N_rqI{wOv2e?gsYvneJE1(j)OS*3B7RxUMqvH27qQ>z-<}{`uY3 zDs(z^#0GMAM|RrYVojoeQ_vv*7aArF)?qeQG(GgGhiaanhS}``I(p9B2RHea)8_y{ zb&;iZx>;J4wR!Vj1p{IW#0VJ^)e`lwp2Qqkj1k zfxljD#%;!0plJ2uTM@*$#F&g4oG)rY!O>S}ek$Xbk;eH_yA*Z#l1(=7{Mf$5*SKWPm#rM(!h@&m3XcN58f@|DGrRNy90*`Vt5Ok1jLBZsbQ;olH z(}k@{4=HjdT7j2Sp;oZO9mw{Bcls2C&VF%7(?nKY5jI`RLBkV zSiD~?$aNtqBty33%vBn(wVROOf{F|EN^aq7NustS>^v*bN@N&~L!tj>0n?nN{GEhi zv|76g;dSINH-S0D+OL4apw%7G#G!Hkm4evMi}&#~5>A=;?d5o1or=W)qM>qFM77ob zi95BG>N}g1v5xKP*ms1s*f7yq;UP694NhA{xTI{5V7U^rQ{K<`>g0Q5yt|r_;lTDF zcq!2t3)g4_jRT$1T5x?$le>GChr(CO!7BG#(av1vrHO{@K4W4sxasS0DzSes&TYrp zd02vnbz?l7lf&%(a@0KTb+^vv+nuuoadnRu>?UU3fEK^D-dQ`^_|VI%%iM2vI})OI z4*y@JU={ino&|pV`+nm-d>fmhBId9akiLJanx-9#k>6-Wc{68sTaf2@J7wlJS-phX z$W0l7KUQ@58%s@E?krt*ZXL(~qU~=$>UiZp#53C7#983+r0H#3}sAktx0NY;rL0xy(W? zl_y20$moKHkWx`&K#)RTTqNRNdcDpy;JqyFMVwAx8lw%S7isXI0*g;mIJ64oF z>MCp7hvDiDY;BT=SefD)eu5Bhtwxm6$jH-ZfB0XWpkx}#Y)`CFRbc=bPmh!FgY`ct z)fSW=j-Mur`jG=0a&@E&AU|4r{HGYPDc6T#mLDL(QN4>1Y7W}Y^D1>1OE;ur1 zFhz%?gL?}k^4&MypfhCBP-eiUst-JY}3i`p;D81 zeK^*BbyFG~wK;g~ZaZBRfM`jq^$NG9`zGmsRmGnj32(;rzUjQBWK@-WKNZN7nm0r+ z1~I74rY;}6vDb^~ZR+TG`Rqbr*9<3tEA+QPQ1!#IZLQypy?xt0`f8TD^QLEiqE=aj z6^I&!uC1I*-{ivVGy{2plrb9DTdEh{NIeW0_;4XLLPpZ>{Lxb*eTQS3CWY*;xQ~r2KaPzaWGzff!$<`L-}93Bp+!Py4Jib}AQaHJlW#ZY=WlwBv79>jZ57fL2d|nW zk(diuu<|`E7F|ShmQWT0$umr;6!Sq|Rr??w`5ft$9 zAA-M>Kwt9tbM zLXA;m;LI5w#yI~u@Cn=kdu)K>K}iSPKU50^!uO>s3vPbvBN%{GBj46_GUV8QyOA$O zVaZ9cK>3NC+QGly@?yf))r}iR4;p63SN&MwTL#lFw)yxgU-?c`ggvnqNR&|jVfU@Z z4%Z(JW?$bFI?3%kSAYES10f~Z>*i06b_cj2$w5_1#bu1q6(M4Adf>luKES1}l>-c0 z=|vR9u!az!)5d;<{XjFyB<-8A=%d?i#Nm$%}GLwMl#_BS7Y@|rilDmrkiY zOT_X3KET|(;b>rQpMWHDQvKaxmedYaEWU~VEbzpe%y@;5FDI!^-PhW!*H5UDf{#DY z;g)Z5BJmA;;|17Py_RT)vRBR3DmRaH4 znCB!S8glApcI%gmm0gANHyy?$$VVDIm!fjxKI)r4&#%uX)DoL}Q1=61I5Zu|VzTg| z%00VqHtBhc3p!lJFf0>z5hK@r-d)Co7Kx%bY@I)UUb%6vq%9G`e<5r@5K8)|va9g1 zH(avj8gNGrl43dv$z0V>ZF;+WgYD{FQ(@~%xubN#HF7uN*g>pRl#)9gZsvR08Ztzm zdT@+n(a<;2BSWS^_3FzGMC2DVWCUn^j*R9P#9zX9Q%~wo%=TmE6x$E`Sc@T&Ny&=h z(WgpUxo3Y$=lVwp#7G*AJHL=$>W<0iYkXgNd2BQ;`thT6sc=YQp{4U(CrmaoLG7xc z^9MQ4urZ~PP{5Jd`DoTTZg#AH&WEO_7n5 zlF`C4n%A~xrsD*g9o!QOq}-_0U@lwp7+N(8Q>R+vsFoY$vjuJmAsDgdX}W046JUiE z@5ff?YgS96%v^VknjFHgnsMt0_cWRR>#p8@G`RM(TesR>&ye+LUV2>p;_yXV7!~9Z z63u}$%M8pXqPaA@=|HgGw5(KhGvVIRUhe!0cbq=aPE=2e3!MY)i64FG%Rvi`fY(dz zM-6o6OouLss8^@a+wle-)yS=MoiG__X=sYM5h}hL8@SFRHQ?&(my1iI>YtfFY1G@E zEG$SlcGI3pXgG-w&wx$PiNu>9=|aa}r7wBhaHaD(b7td9dGx4so{K#nbHz^+Dt&CA zFirEYbbSto8ghLC?`mKf)oJDgjh`jbe(B#+HJ3oK4ow1w{iuylGA2R&#T38JaYfx} z8%+nxh8Abp0#R25b}C)bs^%NZ7V3z5-A3o7P6G`(e|#nAV3UnSstYfWs0;H`Xv6Fk z2I1wSp_>XcYOB%Ls2n&r&joe9w4}j{MonCRP+Y zx4CIC3L=8`Lhf@Rd%+#7@vawdbGo>-_rl^-t?){YSi}9YM>* zT%!P1a9nm&Z)l{5YWOZ8Oj7b#s(d$-$*Iko&aM$YIYYbqmjed|OQ!dog|+~gw6Mh+nC0qDRni(=KwE_@yK`6zz31=NIpl#p2sxFl zZe3xuBi1S8|2i`hrhR_~d7fS`*4z6gDBd{({jZRXsDG2)~ z_Ue@;n%csKDh$qqW5zgHPvHu?Siu|9O9tUSrYIyNuA(hVISeAR)XEXPE{GZt-g2B%(>xcu!%e7vewey3j7aLV)t@%@q+g*JN~QP zxT;pcGceNC?vjp8#i(|Hkhh`_Ihdo`9R_a1Jxuo15IjVMSHXsX_P3 z3Z*6asn@Xw{z#7i4u^UDnbL!mXqkgOEjOa@mzvMpV;X^|K+<_UJqCPFbTmf1Ln0Vh z!u@>}%7W*`P`AU|~tBY&jZ0)2}%~saf+kd#Ox~H?-qNhqXU9nHt-1UG1Y(aGBHQ*7)*35aI( zi>t{VlXu}{O%EF>z?y8oAfV`K!HFNIypZ$y-2uNe_y-(|LyeQ%M$eghPC%Q?4#zOb zXEzU|tI+=;ND3Xms6D6Sq^8)0^{#bDp+rc9i@d3JM^Ui8yl*<@u93A-yRq zMv`fFy75mMoYivumjl+TOYJ}zqxIde0>+mW%?T(trmmsH80$Y!IIm% zsA2D?6olR!0{)G@HFiIC2UW_j#Xx%*$N3Ibm_&4|INB8jR@k8ILpTO+yJ54X$cn?_ zsxu7@d3rNC(s~>Q<-}Fh?nd_-UB4ssfWV)}5@z2#0e#9|f=t>3G zv{mF?0{=~l$MwyJ8?%cl+0q*_q+?P>gUbyRV018SMRp~!QoOlefNuFc91D|Wv&>t#cy$DLR1W? zaxdN(uYtt++PtKclVUD8{3yPf+J@ow@>4utLqc1^~%< z7c!RvD66ug#vjgkaE(8M<;>S38f97>J3$}VErJ`4LWfg9l{dC%e!?zm7<)=>u^07bcJ9+>amskzqNb@N!J8^z(9ZhoKOJIRlEgjwim+m{X}DP|h7@N_ zOmU>ScZB4}hl72AZ7+Ep4t>xF^yCX+_w;s=j52j6jD^|vnS7a{RZF^ct~5m}#VJfH zF5Aqo*C*ErX03P(2eCuv8eIyD5|eYpGB{{VW(`?&cp&;hNy#Tg;@niy2m1p=90TV_ zSOmd8KUovg(T#-d@Heoi`pH5V+{?^*NruZ7>bnAsYZ7}mY;AV;eOsHs*;v_6&}u0ftkg(Z2Uf9>UNWQ`5n1xO$@YIy3NeC6WpqbJ=<&CkZ!DO z9PD$59edSL2VjG%J=)7LU162XXL-rMV=+Gj@bM_EqNG;d;z1h+Dbqy*_Hi!6ky#HEe10>zFT6)=qVUn|3rtsQ2NYrshwU!j!N37N6`eI83?(Ha+ zK55Gdes!YK@cgr&Jm<|X3;l`zURhu*<5EP=RjePhQWMA%#-&muPc$D8^3%4zA+!iV z>(wNx8pbHbNn7xKJjIm_6DR6Cn1z1S&>h)2nd((&sZl!PZ^k5At%rmgY} zk~?f7;ToMi$;M;=jO#q^>Z@B;yRWK`es00z=73Dho>|I*!hQx>5P{m}fV5Y+D@C>a z8)!otg||wqjg;$V)>=AeX#dpg&}@E=goxD@&ft_%g*27sJ3Cz2CM6Kjr6pUCiaXF zUCfRY?ZC>h1|8d=zyNMU0?e>;M2Yk(sjtYrcYPigLOIA~-qE&;=IdItEr3vv-vlQI zT<|r=?dF6iV%Mmfw~_`=RkD12P}^I5BEU&&&`MDITgfB4UOXlp?#_41E7C|%wS zRQ9sMCaA+f`=ycy*C;fjPd_rwV{V>lIosW<{|J(39GMf0671;Tp6zNe4yNU(gRp8j z_al>q_Njz*xyN6{c3#9G&>?L4k7dO*SiNS<3;V4d9}(la+L-)@%49U4L-bbJ8-wJ2aA72+NO!lZ|ud2(Ep@Y?2AeBbI)a;6#LqSk`zgOS-J+c>pa#jj4MOc zDUVPaJygWJ%E6Bj_|~3b9cbhR$Run6JT$OmR6leNk-~aX!(DB>L|ZLhWT)Tuyfyh3 z&2==MrQ_~z%J!46CY>}RX*juGWVmNvsF*G5!C*M}>`Wx%J<6RlPM0Vp#_iZNXgSzj z=jzE6CM2w^5z)ze^@J$ba~p!%0s)BeXj^BVkvWfhYHU^INX9PpXPpN$HArE5tyvk0 z;C`NrRauW47#awt^`Jv|#F z-@9qzFfQhpZ5LOvkUAj+N}tC^;DPzhur+l1x}iGeL@H(o|8wAd0Jjcw11WU-Wd9O{ zf<0V}GcnE%ahW8%u#ZlVVV-LSfUBLJB2wzc+0xP zPPV$i+dyuRfctS)Z~dEFZ$x3sOsMm4bG22;NE;}VC0yM5wGM;sp}nIJhX~y1_jp3g zp1zG-_1`+Z^PKy^(3E8g_Hm(&+ZnwkxXh`KS__NiVC!Skf}gW>y;WHlS@AU3LVzx>V`UfI@)SfcyG2MsytRtwJx zwNDdA1VP>T)VYDqKVBUxwDz;YMj_W|p33}Sa(95QhyK2jR?g#-Xh2+WCoqXSQB_)( zhPQ`;p;RW~Yn``c6Jwd^vIe?)h!q-wLQ}K(P_jPOw_=FXuJJ{(oC~y#^1rYouBh~a};|K8pnjwLjW6|X3*m@-t zMF@h|&EEu9go5vkt<7=G=mxy$_$^$=X8hRez&pACHvSIcu<5-)sE26rX zXdwMjx3jSjCXS(TUZ2QflLtRKCT;rU^eg~3H>u>@N!wNgho(6| z*k4qfj@S8}62aL7vmj8Pp~bAwJ%{D-YNKW-e8IzCb3k)y-Kf*;pY}OmS6Nl~DI^gh z^xPNh;a)8>mNtSBxpt29M}&d~)5}L+ei%TX5>X#M{@BgfugT4BlNlUXay~xdQO|3I z_Y+;J@1wV?KUhm&aq<@=ZX}~~QXEbDHd4rTJv(lkHFNf}`0q{1=}SYuqft6&O(q>} zb8pIHZ3QW~%yH);2yg7;9j=?+{dp^t#QJK@hcb8NGm5o5=hw-#THOAq$1)aUdw-g0 zK_`5?#KQq3CX41(`+~36Vzf`E9XGnXS6jmV@OuvE;8>UngI@u{znDz}&JxDpSPL)) zN(tck`8+Hr1$_etv9=rc5vr>xkpwC?BT1_gwulh_YSBv-)@P z(^|B}p$~kiTBUF;%A-CEGPyd9Cw#J3ExB5d$&m%aAb*lp6yUsK*4?2u=(e@b^&O;o z)O%S58Gf+W;kWpNA!FEGm zFc26?PXN7zfM9{RTD^*G6l1qRlmEo8c*XV}yn6*%-wxa$K%Ojm4T7Pu+Xs*hZd89ajT}v*!DZrq2O`z6a*O#5o3@ z1*qU*)V-z+57Pa5UuZt=dvqmMYucmCDOu>?hlvX?(Yx!WLgzY)JCYeL4d2d57#pnx zi$A)348YL+^sZA91&gcp&CHCGQM;`e0Bo2hn=tj9$HN+p{IhfXF)PEDeaB<W{nbQ**$dUBs=4RjL6gst6}X-#KTL6@-#&KLNgcc- zsfFWC9%;a$-&OAs6Vf}xv68{1hM7zYwn}ET+|p@V^fLq?!{QfDm~$6uh)7S)O&Aus zs2HEVq#_~dSFqwEV@pv(@p8=7XoC>0G0gcjN?j6fihn@VOF+Dk{WFoe!qjOXm$n-j z2NrJBt$V-~{gaug0W7GNs&1~N$ zXKN!nGMAJGG6BX`h-G=5|RZ;$Zhg$A9z0A;QCIa3=;_0b9uPmf^1V zjJY^EBY$37Vy@@c)2$YJ&r%B173E^1ob%lBEFaXvd3*W~hxJQ@JAO0{hWh*tbbzt% zbCIb)omoz+1E_|*kBRIGs+tsyH8&~7%dls<-Nm46er!lwi!2x+k2$MJ==F)>)QHL9 z0n&vAw!;}k-F+)+crY2IxB6AtN`~>~lE^FAMKVHi!-LCcm?t$?g>W68(XZI{O#a~W zZI_AG@8&+dqx|fPtKHKSc6swUxx#VgliCH|VNPK-yc1`hjyA-t)H)Mr*VyOmNPu%; zv)~O@u0;z`M~xAW^hg*WGis3O}6ToLCWu;&zOi=Z{Qyh48xIkrVK7B#5Fb4_so$N^!;y| z4p6UXUt|QiaoxmTX&&N#XE9=zJJ30x%{ns^{|X_mg`(kHiXpWtVutw4tiL&F+;F?= z9@P5^J3vxu+pN_+lJ)Rw8U7-!?__(UtL0F4y`c&%haZz&enwS_e~w4byFh$l=<5+zU} ztG#NgfridA!bhDe~u9UtwtltC;zhn)N3Y z=VVqznmGK7Da~~7RORt{x#abWZsl^c$BR9cpuZS~(3F;=ehCcY)86#Bx+LT;L16Zv zd(CIhARfE75Jx7i|9V(-aGn3U?Y`8)GPR<6;q=8h{B;I{4(9C9X=i(*C7YcGiHOm} zd1~z!ZA11wP_hu@MECS3sb?y0ik0bj1ygNQr6;q#aAJ zuhnIB(j4(?IkB#nT@gSCmb}QtdBSw|fO4*bbS?t}kZEngyyThqIY7PSq(@z0aRrB1 zp=>s(_|5hbm2Gu%%KFf-D&le+K!%l~fMhD8m2BV4bG#9#9I{EAB zd$U!ruNCF#9&eoLv>fo|&zV*bN7zmV$8T~39d9d$D$a!4T67N9(;e}Am_@?0;bCY) zaRqDexX43sxS8(c{s_u+|N3Qbxk&l*uQKIt>Aa|E_Z$o94Qi!4&g+@WoB%{*q+Am> ziH9zZ7?~~2UhjociA87OV${==v|(`EdT8zy?muC$<22heEq&737o^n|5Br~X%MS~H zd8D%}e#dg`i#^+1;04Gom`5*E_9uY@@+)&ea=%Q9aDZTT7i|~2-@!<_4350Dta}n1 zMab}(4imIRH6kB5jmPyQksz;={3wQNx@i1EYLp?9=Onr?M;=G}tJ7p}jUb$6LuK{- zERQ@zFliS1KR-v&dA_|OkO|+y^3>1tE7mU9{3#U0O^zwKo;yl&!0^D z#LZQNQCGt_0rK_o+Zsf<0`FH9s=14}e+I0^1dJovuq%I^wGOdU9(Qg&NCrIWj(w~! z)ZvKb$tAm|@|J)QG?d0i5vY;hci%P7@{5{k_fK2A6BdY*V&ZJBK> zw6WbnheXvbHfGGoi#yBDQ)B+~c-d~WdxfxE+P3h4IY5~L+YUMG34_`&cKXNZuY+Gj zR{(yErs5N0#=R-C>E#+F!P=@onsOd(%)!@i}Wz3X%GbmP`~ zbw`j=n~W_THI}f{8O~TkEkv99y8bjlzY{3kZ#UeHF6fWu792jsFxFq}kOXGUZH1cE zmbWY!3IoI%A4^nk0!6oVPEz$&RzLq%6n%KZ5vtVCDlR2|A45k$=1t5s*AD`PnYIvW zo?yf-(IVQYn7P-2(StUDZCyU6c{5O{j1CWk8aB|(nc#}ZjJhDWD?VDz3!@|{vg-NuibD25e2#ryVsa#d&1?!qqmW!ow`$f<7$4i;I~S6VQpjFgvfb=ZregL`>i>8&gj# zsYf~ni77EZ2XVm<+LFoHoVJu2yP7ABef#V#-A$^21Fqq8!Eo}jm55!}Mgrw+d4L0) zO6)e4cw*v=>E7z0{h^yD5+Sh{Un0)YiXyaImj+8-7X^L$|0!h9sU<$>n$8$6S@V+r zXJNj;{0D;B0ku1#z=ICu(hI{NKl*=9f11bHT22qh25l$k^a@oHWZUW;@*Xg}QG5xj z*G^moNuA_8-+4NBRuqTXWRUvuLI=KVTYWJ&Y75yyCC)hE2eVj9(jDCe9IaCN(F;Bh z9%9cC!g`E>Vmt+cUa_^rrg|$k!B_ES0RNfTfO4~?S)EFba~#=VRiwS)ae;8PKRljE z(FVcZ?@+F~V>#=AOc2mDU^}lX_CgP)Iu@<;crYJomfC@Pg$Id)j6;Df7_^lSnlbze z@aWHaRlT(Z9hiivluuOUY_H|$;}Qg0$((ck`Lj|GMFzUKy(RSbw31gwW`ii3Q9=vooqfIO}y7MUg2bPdwGH48563`Y!HKXcMlzs0^6paVLndqcx9 zxMs%_GM1yiLl*(3YIYPDn|>i2uTX&9;CWh!u2z`H9T_(@j0#nrFR^aWFmmu#A_g(G z58lBE)-faMtaHKBaWG;$4{*5B;jRz$WwDChe~dq2vs5z?k0kM_Sw24e?sfVZJQ=(lUTABua`vj!1`nO=CGAr>>kMW%rUceFG>krQQoh_`ata#=a52{ z7zSeEnWT?788423@Z^?e4B3wbhee(%%kuSM!EC^|Sk`8Cwb1FHR?{4VWt2YRT&4S@ z3fZ^!lXchlcV(a;q^H_`)dsoiecDfv><85h3SuT?Mq<{gqFgY>+ zpC*k-MC3AKapt?s+L0a=Wd5$Un}xl8Y2sE0ER7Dag!~;|U{)N^s_NCl);!#m#B~(^XvKL|FWr@K4JD0qGH-7hHFST$>z46FpjV%>?|w~WtOfm= zdCvvGgLc9zWX?J@0(BrhHiFgRJTgmgCbCVbp7XJv)6@ZjCS`;z83fmdistkApci67 zbOBpGY_-^0U4VlmXsCaoQ3J*LH%-Z3z0R~<@e19a$OEhI zN9GR|XrbX+Cx)29Ds-8_6)=#Jp!2EO=stSu>`)s(yCg{0t>&|qErl1oVU#u}==;O^xgT4@z2roXQ^${9@cqk8!-8SGn=YfUR+rzaI%yg6iHIi;^s2xLwu0$>r<=k5`1g6>)33X> zMIEyzGOt+U>HN%EgrYuvfK$jW3tlVOwg|d!I@X2^yvFlTRCAbX5{~L*v*^j}55n`DPZ(Dy(uGCq zNk!S>DDGkp@l3a*WqoL7Y>e^c8#_h|JbBEozfrhC9j@$=t@M4kcc0e zk-m_#@2>39z^VYM^&o?%nZu9a5+>d`-9fpC_)HSq=KK4N=qd9o%v}pO&ci45KZPdmuH=LVE7e=Sly4)K$_ay{;u{9 ziecvL78#mby6@Y4LOp>l&kGuNFFS(tmS)&T;V#-USX5WBFOcHD?Ck*^eFpgzoGt2> zz%%;0`yMk(K^NW-Mqo{mutEmcr3{<70Sej&+Tg6Ifj5r z%mb14HY#FM@Zg>VPOx|lxL8dHR^qHH zn(q|_yJp9}Q%XUOy2umPeOSuI)ui*aet{b;VF?{S25*!ZQ>@`7_mZ@HeWUj&F*SqR z=^3l3vM{;}J{&(663{<#WoLDM)N;G8>iuzOWB}_?OTAvFx!(3MIi;u_dvu^Qq#fBy zN2!Z{BwWUn@jc?d7vg~CoY}lT2ww#LJyE%kw!G%~09-Y}JQL@r-=)oJW|t87}f&XPL52zATNX&Hp1SLp~Pg ziOhHEzjJ_VY%|}56+8}SPI=xp^N&x{8ka)3%}gZJy?i@)EL$#Q`hw4+eBZ<)Pd05Z zIU_+Jiu`Mv#Rd_zfV-O0Q3V5Fv-Cdx0IkEQjT+r2T~w0N64Hga)_zI1ri?ORi9YBrMBJS`yi>Z1Nz*=psN5P# zs=ql}^N9iihGT3NYTf#xtkokT*9u5!y)ip8Ms? zkR&M?EZVJwwsk?RHfHDx#qalf=%S!0zUtZ7HdaTWg+?myuKuel*lfJfMSh}~9g@>F z?+DSkwYLNR6d8J|6&f|h;*mijWG_iVpiW+n*UMh+v@x0JGq;aV_P{Pc-6 zDq>`myjV$^F?;0>wbLZD&S|4UE&->L?tD{4$3Lhw|3=NrU3b2Ug{PaxvCdJ%ga;;u zD3K8N>?|C1&hP&b%;my*A{XA3YF#o^&b6WARzhAmh6lX=<5>RJ)i6wDrwO9SRg)nG!xL6!>`RHlpfR^cJofEyRZ$j_&dLK9M1xA=*C^Ix>~3X&VZj zR&^OpQS}cEb*O5dva5-&@xPt+dQL}!)gZv?J#cbnRl>-NDAIhgf8SHvwOeM4N`y0+ z;TVK8j?qE0kSbTaI@s(e$Gw7hI^p2uy8mH8O%f17DXUic*j)Yz35Sb_!`hrT(-Ps; z^xJ`JipPEFXVm3$Sfd*}@_$8OKdAouNtn=XaW9|85Kd#~zB$ko=0|d-O(jQ;pfI*u zTI%P>v-2-23+aGblhXL}@Wwq*cnMI4z(>sI`~K{G4rv)NCqqMk6Ra3V$OPTLrK&xM zFkJIJc2jV86$Ci)-A%o;dEN;2FLXILMIE-9vhn*H3a~*PpiE&@D`mgA2$_Uf+vc3* z0B4A(e|(L#)b@mwbMi8feIWz3?z;Q!3==O5J3q>P%TXh((oQh1;qQP3L#s6yr*}$$ z>DgSnt(8xmQbNCgX~F?%1e;{v#2wt;^#3$BFW-G8Jim-;z3#VhV`|+dUd2FhCPK9- zemxXK8X-wmZ}_w~p4vTq@P`~s@y<^0;Y5v~-czqj8W-a>vk7ioh=`Y`G{dHJ-3h}k zSZqQ7$)LOE89#Kruc7p0CAZ6Noj^7A4BwOt`kI+*ss2?VgCH??HQDJuS)p63@7x!d z*3g0A=e~PdAmcp5(LgM~Zz&H9QeaouCq$m_bK9JZZ0Oo!x(g_Eb6m2&{?!3ry9eRb z;C~jZ-}6*@`kJPm5l$Qpf|!;l7Buo*sarga;+bxG(2hS=pZEoZGhB;vqI%@dd z<$r`azqKsltork=Vgi{PGW{k7<~b*v@9S@%2!D+6H#D7P-veSOrs1yj^TRK=ca>^_ z<>MBk>@tncmzGyq3f2Q3Lr6UlG`1YXaX-I#Ie+|6t>cnyLh7agN)yrMgDW8l9$>Z( zg2<;^e{m@wi=JLey-@5^E%zevM+WVwJG#+VIoDPp(+-Rf8o9gniX0mHiEJaj15b=fuTJ~O)PfTE;t<6ezLsE|6@tfs3Z^Sxlj9xap1yYsIDG*1WOf+~Cv~X%oK9E-WU-%^Wje zHvY=7Vjvj5^RkRS1h#uhEi}%HjZD?Ht(}vKE`FeygJ}4c6-j4{DVs`QL4yA#BsWsFPK$+`Z zFF3ag5B+n0b2^$2T2m&A4^l#=qz$*aA$J9HeF6 zen;@LtCPsCzGmud4Dl4M_XL*k*0im^ZOL1+TApBLEN{ihl0}~%r$tCnr(ynmDRep5 z*=%@m+SV4G;~q0#_MNblWG3R`R_O6Q*t-f6_i}Y8y_$6njE!C z(C52h^t8^Uv4NX0eqXU7kYHd^lV)zjQ{1OPZP?e56gfhM)>J{ft%;$j`Uj+Tec?w9 zI6R&P^>H0JS83UoYss^vE8DI|9PzUr?o8!=aZ0&qQHBkc@AXM|$KUm13?w#Pz8Y}p zSDRXyxliUEX`lci23h$b=QF=QNu?TkL(9Zgi}#@7{;S3X&)(r0JXMh41#Q{c3xLYr z`i%M{>#Co~Gfk-iMg4{vcaZ#UN_3&oKudHDmm632IH&7f?uE4(>3bpr!KnA-s2W3g zmj6AyNsV<08Tlr$ROmF_EtbiA&@IJMhCj(Kn7))W{MW5%rPilJ>aR|l&Y>*Fvy(j~ zV*K7{x4&gWskHMQ;zDLNfhsl6mhn@2(a*QB7V%?c$10fouuHF;ZDcD z=NH+TDIzdz2n^YVGSce)ya=ks4*I`R&itPV$B*Mf?o5%%U5>ePG(+Xck%$mR$|ys& z4n=a0$<<*+bCx3|HurKLnL=jK2$Lf%Geq@zPVx)8tHk>V~p)hPA@LS7=9?i{@Pg{IZD3B@bLyrHtzXv2~ zR|^>%Uh_4s81PW_qvk<<+_?4}M2DTC4bvUO(x0TAMa=RPMoJ`n-!mBGqSv6vp$R46X0q z(eRq3#Z+9YHN+^gp{%Zx((Wan&D+qeC!Za?+E=YAR8whY=Jc#9|6Aw0CE|n?}m z1Hz6Q){wQOm$$p-h9Y75o&-D;W>j*L$RdpLeNhR%c4z~m;11x7mF&z zw@2<^=Ct~b8~ePElQwQiilipOyvXhUl=cZ#-e^^8= z!~_vY2bvBvQ1!Y%57qX>LOM80vVc5#KLz{>F;J084rFZ86-H&xq;220ZjIr$R9TXU*G{3o)`b zn~Fk0yptO>0v7Cm>0Q3x(8HF9#b%S>CbhJti|``6(pi{N@~3ia4#OfJw5cY!M(dDX z!`U^zq-VxY(`7u=2@7`xcKV;%K(C!XugC&Xc~Xc-u98k~)RVNhKPyL+_8-y5T7E>P zx(PLS9`lA_?a9e=TW=`k_UW7iJ6SJDDnshCXMx0{wX=z;FR`VeuT>$yGL}LcxdhD_ z7?Uq83dax{0sCK1W~u|Gm3k3V3a;P}d-G9FPPIK--tJ=G-D7{zG#{G=rc-HN?~JS$ z+p?HeWqJmOk>AbAKt+^^Wk9rEZubsJGAMWMhCEtEFZ{*P%l; ze?Cv+h;YhOr*q)~auzV&>ZEfX-0?1en;J)7)~;p-o{~sz+CjCe~r9g94}S4#MxL;L*8tKn+4^)C;+fk zp!QFG(;Ufs@%)~P=l6_k_`eQ5WSQ|S&99)%?>XIz36_zPG$jq|J(zaOy!D6m#(mam zdu+8|5@-@80@Fa@oiBlVxd%Qrl%CN4y0tOyxeD)Dys_IunQR^e%oi?x;lP!vDxfm% z74Y=b)gxd1wY1G~-CK(cZQk9#1`L{sl#ktH_i$7we;6X0?4cmsOm;j)=;4l(zWeQV z?&tWJ!HuumXGS%oM2v;(QDwHN9xVfy0lPbTm0>t0V zSS9@O>cNE!S-(+LGLa@fBbgh`x^6zgDF<4KK%$o*`R|UE8|a%uuoF+_l}J)X+I4DUkc_rpQ6zyM;@n=^ z&Tg&YO0Zd4fyTcmuSgX<-x;=h%}IjdCaM>NMddTr4_sZ5wv0 z*o4v0_NjzD*!EbOsMs+0hl6^u-gTH7|7y(18ZXHbT-?fo$EKMrZk=02&XaXkb%%&W z(Vsa?>+V6#2iN2<{x$yVtohd1_jIx)5yVQOy>%nt#{$LYy_Da$Hqv{& zMck6L9tA1v$7n&`DwX8ahLs}XvDW|Atwxk@Y>e}+o9K5+;^g!ROjE$%=j3j2OW3j_ zO89O2x7hi&(1J%7@5coKOQs(Jz@jI#9+jxPb+wE}JPcz@1&Z#-C#+s%-{?u$?8Q0> zs~++@H(AD7D+Zu4YX+beK4^s5!1Y+e%;rQUaOAXoymNc4sGon!iUy+@Db9PAabw@3 zkAU(8196<+m`#INjiKm5);-OME=*11;N`K}|S9C6#E&Yv+rd_V_@tx{{ zJ>8y}D#jL}!USvhb}4`o@bLI&_HOR;GyJrXSoq4yyZP*v!| z61GZN2NaTY#e&KeX)#rIxEQe-d2vEA_^4<`I|$sXDE)qAeL8AG9Tie1Hr9nU+| I*#%+#2dtK;n*aa+ literal 0 HcmV?d00001 diff --git a/plugins/deep-link/examples/app/src-tauri/gen/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/plugins/deep-link/examples/app/src-tauri/gen/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png new file mode 100644 index 0000000000000000000000000000000000000000..1d98044f13bdd3afc7cb426644ae118688c03c14 GIT binary patch literal 7971 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 Z{{b+Ide7&7`HKJm002ovPDHLkV1iT#V`TsU literal 0 HcmV?d00001 diff --git a/plugins/deep-link/examples/app/src-tauri/gen/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/plugins/deep-link/examples/app/src-tauri/gen/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..081832466b4a17180fe9255fa0d3fcba2010592c GIT binary patch literal 12392 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 K@hVZnfd2y&>ZwQo literal 0 HcmV?d00001 diff --git a/plugins/deep-link/examples/app/src-tauri/gen/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png b/plugins/deep-link/examples/app/src-tauri/gen/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png new file mode 100644 index 0000000000000000000000000000000000000000..a2a838e7b550ac82b12e73d71427c6ffea06a944 GIT binary patch literal 29506 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 K@hVZnfd2y&>ZwQo literal 0 HcmV?d00001 diff --git a/plugins/deep-link/examples/app/src-tauri/gen/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/plugins/deep-link/examples/app/src-tauri/gen/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..b18bceb64d257ed31d50e65d7f2b98abcfb21158 GIT binary patch literal 16751 zcmb4KLwhbtvwdURwv8Rzwr$%scWm3XZQIt4Z7279Kj99#dRUWwdQ~k%D#(e$L1RM$ z001~i2@$3L?*9KBNbvuv(zXo*03gUFDI%!ik$tTT;e)!e+PBn7ktHSs9D>iKBwDFd zOhrv73N{?p(}vJ>`|e}p+gi3h5~Oq$5kL$naZCDyECEbptK2I0IorVk6LFj|b7Ph? zecu)qQqRru>hDDYj%5-r`2Q;_hDOoih{555^BBS^-`a1vrp@+P*V8IrO+mkFe&Qo} zgKd?S*mH9R0bEaaNN2Q#IE=CTqS?}oVYi68iN~B_f&MdRUxkzXWaF5lOKg~gYQ@}O zJkYLqXAc{F#)nvoospV=yV1zR3yZ~Z6RZvHS#bZ8{W@D(X&*nuXaAnk?IVP8Jr9<+ zex_q_dGKWFTY?8TiaXdM%5qi}QKt0)g8&QUs=!<@fg+bwx$kVUV%e~b<-VvMv@s?; za=rNZSi_81S>=%FmztUi5j7M+H}1%OjmR95zJ#N*T|MGg3 z%^nNg5qwoe_uuvo=C*p74>)xPT97LN)OP%mf-D(u5D~dWl6j3n$q(gx`PkEqsE<*1 z{6&(Ruil)i;7wL0cVm<{^Z=8eWcv7ivwNy1UdmOV;yZj1c5AtJV1t|N6A0EknY*6I z6+eI$=XVVn8LVoZv)8~(!$pA)e|CKg9nIKyAsDyd)|3Xn=9AinSb zvQE#W3^SuT`~+4H8U9mI+pLUmisCG;1&Z6?JtQ<@$<15vZ=EN9L2gF#>^c1$WreU{ z4vNYfupSkWUgquvuvJj_b*WI&I%Bq3OSMbKwy=1DcVgjSTJgof~I_U#&u3; z*J#Ck5@C&jY~b8@oyFC-)KtHRcllaRX*M<7rC&IhA5fA%h+>SGgwy@(ApLfotfuSV zNlSLNi<@3d>-RiBV=Aze&|kSiKyxCncPoaFx#Beey3 z|F#SyPs$rq9?x?W5l8e<|Jc)|@QB0dEC2N~OP}XueXVMW{fI#TGez9Iw>Lu+97H7v z0Es(%RH5mxBRXN5*U9(2F-ljRMs!D(M_i;^2ht~p4A243X9+^vb}$ID-KyH{`R4aT zsN2<4m1rJEAz3MkNEiV4>t7VDpDJFIk=_5>fym_dS_qAdOAH{6z>j~!Em}3*&yz(| zOe8WMeQ?~T^e|n^CXo8Q&0TvmPfM0gFFz85lz0}z!i2qhE#{-Kn$*wXW3Je`bZ7V8 zN+o_u9MA-{hP3e+TwC*oAOP~k%$3}i)XeqYaBXs``*l)%SA@-KWX(`(o1(pl8U9Q7 zrnosVagUqvYf{y9)5mlqEojP28b@2e*}qIu@f)5g@4TN9pq+{**Lz)T7`etNbA=>P zc`cFvmQjrI7B&&dbPyB@_nd3_`71$615hHk+UrE#h=epe zL!A*!N;;K_gILY>KmCz*$9qT(;0+n^E*t>3X4ebA1;nxSJblf`hV5{ZT}w-tagxr1 z625~SL0N((5GJrU0gFI9-1;4y-Tl&1P6nP1Ux`6Q5be@GRImJLtP+$y?}6KV)=^{#u)Y!e z28$Fe12-H#1ts4~(|`5F6=i4)kH_RNa#Y$3banLGzQD9Uc)&T9)^J z#!bUiNmCZ~lt-DvB$z|=4vDJ#Lh(3R&nsEd1L|O8=J$O8VwWW)Z2S^G6)5!9tm3pt zOpSrz7zrPE)W;22V1*IQPyLgNNea~ex=F^qH$ES~x72KEHc`xt5$N>vJ$Zgj@}big zf;F|ir&R!5`;S$tde=7hyjxq?pI=g|=|8hCG1*_7Gb}SXgi%>y{!3MeSXJ^1(MQcc zA&@&LYH?20NffU`=>*u4m!yCJmALWO5)`oGi2(WkoY=mJWx<%HpIud5MVcQHaY~H z`9kF^J`4D?nMFS62>lEwYu)#%_e*pBZV%poAFAtMjYt<`uX=VYZPI6J;z&na=g)RA zbzZKTdl^2-Gs=|6aaLrUQ45S26b_Dp{d9l5r28&%OCL!iJ7Uq`C}1Y7VTfC8-Os&h z^-R{L=%j8bjwBsc<;g9M*cQjoTOYTF7ij`6m$AW=<4+o1=pt!tS{j8yRpFv%XcRD6 zG83LQc||q5y3P9)tF^0Wj@oWx@lSvUJ-VU@QGW=TQHUq-LYV|qMJYWVvrGz%TBjcw zcJN3tx`hL50EyuIvgiFOxiJG?%2%NZ)l?@dJj6U{d}H>!gaMmvt3h?0ZHqU&$0_{z$+%N-=H>+7$C?v|4~Pl5 zPai$+q1ZqCd*80>0CxV>zo*#AP{nPG)J1bq^14rAYO#ljNrKd2Bj=)zK`aeCNSLqL z3{iDmKO04PQH@f~9_`drOqfXG%7~1UL#}@%9dSL6cFo*nyE%@@4pUn`4Xx=hUCOeJ zr)A{YsJglYi=~UBkw{P?v7^EBOXGx!XfFP$Z}y%tHuMnD6NL_)i;5?;HTv`y*>rus z8wV5koxWK`yBU!};9YCjk`!YDH6x2z_R|`nS3A_~_>X@My&Xsv4TLyK5nqEDSifx( zX;=TPj-r!!1^QiA3#VnLF!Rh2%({<6Bfjo-M;#{NbbD}l5^)#fhA+J073ew zJ<-UBSzGsQ>+BIThCUdLpjsi4D<~Yd&N6PnPv162Nmxmv`pLk_5@f14J?Okg_CxU6 zuY1o70Q0BH-vKq)t&T!~fmt=9j-S=~l3aeWX|^gFb&wJ(uPJPeBVHYE+j+{as&axr zONlV{oiWyFm89dWAYrM>hfgMA*x5c1GBwaKiKr{z8M`IL9d(rOr}Zq`Nx_5hx5}-W z-d)Pj6-b6mbnLRc#PYSzJ@)MX3(ZXBEUJK-V4v7sU;h1}Q7SuXa4>}C!^GaNBg1y} zVZb@Fxj(MbEE=QrPa?Q<^x$wc{`=fnFFi$y~n`J-g*6&2CQ?7ep@YFKjxF z4QIScR{=K*vBpB+!W2;!d*iT);&9y|S>e`u0~Zm2E#t>6W*Q zbEIQkUYnvsltPhdET(|@YIx6S%urSUW(wRULlN4MuwPPor6 zE#>GX%#~O;w<7MiZs0ym^v0z4vU|9gZAp^!>-r;bT9haaU6GEMS9hf>;Z=GZR564y z_io^L?k0zzi36)3ylDQCgu@`25v{EW3e=b$zd?$&*r30f%JBApvDR(xU!xJOn9YJ9 z5+_FKLJQ*Nh$Zd*tKf9B_W7H|Z~e^~Yb}_>rq}|#r0^wam8!={d+uZcxY8t4tVODH zCs(MyDiGJUzBq~F#-|_;qvJc#f4*N_+{mM2`1JCPExSlAHGo9e1RkTHboJ~tZKhg3 z{XDdlHQV;}9kr35pB&%|w_;8L{{8#>o}A65e&RC2fP9?)j{V2FpchY00a+F+mlgH! zrwZSjs!o43g){iZkM0)*lt1+CgjPOewmmAD>3n{NeCEZ<_b zzEUeOG`8q-__Z{k+VDWP1j7(`@x(#|)qqTy04nv{hTfSMA^6x4){%6P{ z;*_dyHsMIsRGI~PWJA7fB_$Na*(KBK)vf89t-p|*D=j*NkzVq{s@_fJlKvMRm_d-Gi`U#OZlMT)r;FuSF zd%8A~s~Ds`QypZu?#ErCc!CdvPTWsLXF$P!Ib+qE8${^SUZ%5k9oGIg=M{pm7B+bV zA!Ut90{a`-joEa?OewXrRINOKo#7JUOxbU%y&-*kceiP8$LR2ByLLK_oq&Ku1_ zLwywyqO!mCvqSlePbj+jrpF{HWvB>=$(!hj_w?HJu%th&9Q zu(lB%ojIyyE329Z0niV937Otc1SIR>j_V~wzP(&))#5SZyo}>+ex<+I=NOV_?ODC1 zuxOsact!yL8!8}-PUN|YASH$CeR<9o7i*0^`A}y2yL%F%YAQRLIiJcKVGy-4&Opi( z^LSt@EOl63jyy@Jsr_w1w{3`Y;%N6uj~`nI08DU^yUrc@F@4rv<&$o*CKf#p3JSZY z_3#Kx3+gT@)A%=L2@fIb>W4}0NVes+&BdTpjeZm? zZpwm`*=fask1+tpTJ({pjC!FfF1wje)wJi-u9W{xjP3~=p4qtRh*`;Ys3P7m>^zJ4 zU)Cp>;qGmapKtG2D}I<;4H_4(`~KU^`Lpic?;HehDYBK}aCfjIO#_Vz3DP5RiawW- z`QZ~6H>5EUBT}vvo)r90oAv7r$!S>THtdUy0oPV|KXXQbmKnT=K|xCR;$9QGwTayO z2XA>(o?o}v4%sf8;znc_D>4Ik+})Yq^PVE9@IEzaYPWGRGL^ItmaoV}I0~_?Y1$5Z zrA=jy(3n#!PGE@GbJm3M%4dSL(z)UGay#hIoHI)I{G!l6x{xsv$a6i5oSLQC7HH}+?S7JtnuA=cuAp9)5W7zzqt8}KL9QiWAisRrq9e;J_3fu zCLl&G#>XIWe7}xCrWAFtZhygrIK#f$Q$@wBl$NM$`pKR~<09fT%PKAn^L_elK z%$vCbAax3H2s`n*9glUYxUwy{X z2x$YQdx(Mk60CkqYV}l@i4K=-X5l2g-Ij(nneoc1EXs*L=`9V9s)Le7QszMw*ASN9@lPDq@OL z#Buxc7VPuD$iRxIEroNA2>|aavFCUH%`A^r%ZA^7eaI^MD9QB&v%d z+5Y5OBK&$M_%tFmHU~xfqX3mtY6uBpL~dNgV8Xy*DfMyN_>`#1F+)6li_=5_cX0;t zj-VnL1AP1giNxhV+3;xZid>F~pELTwBSDAK`&LRtF(@g=mdO>M;zW)y6Y%f_?C`9N0cmmJMHJ#OiGj^YZ3Cn z^|?!DS_(>6Ba#3=n74pZ(t)(l+2mhx80o--AkF;G zYg#OZp2wbxS8}0dapo7Ni|QdZi{^WUTKaQ@`()TB%21=$Rw zGgugA0W0FYzz_{?2|)zx`*1ikPYo^EKdPw2BMoB!t)sBTvt04pCYMW2!Ta_htczkL zf<#N)toU*-wi-V+_rp8*&|jZBQ>NGYtQRDQC(vUFxzd9t;4Yjz{uhqbSw^j4s_Ih2 z7o7b~WhE*CpO2%#Qxfv;$XrWstG(^Tm^U;nIkZJ>!yRyGcyTm4CbBP2ARNu&kg2j4 z>2$6m&WbzGg-fqN!S=^cDc=dz&d&Xps;*OA_H#ji9~c-PJd~)uKq?8nd;Ky-$-TTb zHH_YG^PL)7r1*;b8e{0C9#Aswl0-jI>)JK<$*W~>6S0An<)Z#y2-($+k`|a62e^X6 znzr-YYepc4s6Ds)t%@KoSC~fj0xGxqNRMt>b{($FgoY$YdpPt-P zSt{UI2}4IYxd@q1+Tn~v+39YEsGE$Z;e?HGlXXkXiY(X{3>_v~N5lq4Gul^pL5m6( z>|%YYc$37feO7>6pCBW-Ogh&*lgY5IlPG5@+1mOI!~{JvTNj~viao#IZ}g3+^X?!O z0x)(arfKE%=V$}Ly4+c1V#e%}``+(pHVQj*3}|FaeY=l*${#U>A7I$CihUt|D53a} zCPA}->14iNtz9y}Pa5KvLlu&)Gvd|n_0Fyv5mJh>u~OwLzKH2GEob8G9*CyXv}*kV zW@M1cXN$lY(?sI5mM!Uj$fgH;4jV|ADkfzG=@;3;4cgIeaolj87K8j4>(yWZ{{={( zY-n0R4UD}GX!pEN#fa-^;oIstEU2)YGj(m&2X@LDV-wWGG5;2aow^4umxP&fG7G!f zReK(IZi?l5vu^z)Eo#w}O6XSQnuDOP^US2GHRs+!wCOe!))0=$uv{IE|HEB%h8im& z-yFpRZonyGUG$mT9wRMz4YjCmWTmkbmzWpSSAM1q_$4#?Q4BIeR7)I%eVAJW<Pn+(o;jM)&Qg^nnlIcK-mr zr~pGpVkdY~RauLc!XBWw!}{nfRshAoqv~Ya>XAx{T)=tN>w5O8W*b~t>}_IsY$AA} z)|1DMr6KymG=p0IZVUO7#1Hfqu6Spx#tANy+eenr%&RHF?kn-ys^;v(vu7vcZ=O}` zK`)%B0)(HnhoSJ$r>X46_*u_zIvyF|Bt_!jVqn{>f*YjE_vKl}pvBr2t?DC1sZDZ@ z3oK?J#jTsNM1LAl$B~$HWh4z^Ay=F1F++!dlq;`Hq@_%99`~!q2&HszZ|k|H%U4<( zNB}h;odyy>D!OET>Bm6NymfzcGKuH^S7ZW&9p*Cn9IHYznW%G4Hy zU;CSc1p19<`$u>e4&vck!*5W`Sy1en3GmB9nO#unBkM z_!~Qy?vjw6b{QIifxfiTVY*s53fcIQZlRK}ARN_~120>KS6b69Ve$dgr`*?0_A38<$$~U{}KFJbJ zzfm;~bD~7Hta-9i>S4bkq#!v`qR*{g=O!WS`ZV2$Gk&aM1QR>B6jn340j{Ik{$z*( zS7McQhewd8&Z)NV>8d=aDs+f^<`&ih5njoch<>%x&?7Qux+#M+`!E(?=9kI~5mV{k z-7DW5^WzqmBD_H3pkkfRElKIV_fAw4ZMit=(0iGbbV}a`g+Q^6>m)tn(DdpxP5DMI zv>L7YgDaqQeur^@>vCv9fg*ku?6StPp9)^wGWdQTn?Jj=%j7{KarGjWBz(BHa^q*L zzR4<4=Vn6VK`&4uTO`hZcKeU*98s+R^gSXI>5^d|8w;<+Ow$rD)Lv)%4z8lL@G7VOekB}&(9y-N50Lq9ee z7f-BGgRKnP&zMI6IxCZ;?Gk;& zF5lKk*VN+Ls6jRLXM=;J;PG;QoCZd|H+HtJm$BH5RhyMxW`*gZ-Qdll{yQ=i8dCPd zIdWq7&ufqRNKyE?7-mfa@8)4}t(K`P{tq0<(!-x@V8yW(vgPM&AWT+&R80wcE4H>vR+W3N78{3%MSUuF zJi08FPnPQfF~tvKDfDSJG+*0!u8Uvn!;oWqk1W@D&QsUBJ>CMvE{q=r=K?Lkv5q6( zknE;`3w5GE3Wh%II|lWXY9WmjjN{G6ox)K-fIp->@`{t(+-&vGf>Z_V*fq_H835MN z2};uD{fpCiczYEOYvri)#k2%dPe0Y)S9=Gb@O#9!S{ZAoGFu(r@Ev zFRh8i!J;q9q7PotE_Y$3w-fZ|gkjdESE(ZYXaUW@s(wbS?VB_Hr!U|sFRDI1c@s`Y zWC^seg(P)$2ZFgffGhx6`Fl?NybnmO^22k&XJ8M$y5W_Gx%2yxo z+maQupYBH!<@fKS+<-CRw>;YN%FDOdluLgt1E=#1!sE;}w=O&qVaFyC;r202I!8sj zgYQi{(W#*|$p92+!c#u$Qp`;9293zmH8lODS*B`&zoc*HuF8^h zWxVf)HRlX{ZNc)=eG2q+iVjDl%mik+_gst=j-ydUI5Z2l?&^|_RU%An*Ni;h?;Bf- z`q{o1mof^7U$A2%(_bdZqwUAKtGK%dfFHO6;68dn2XN($quZQz{on3%gP$|T0C+b( zsuC~}`%Glk4PgTSns_ltEl<{(Xs?q(7PL5j>wTY|Czs*F)0&AoR^5j$@{a7wEBfsM zn4ilc9%HnQmdGJP=ABZKd!I+3>oCfH5|vF2Gf*yYti=Gb$y=olf7P=t!**T=DDE!D z8x1Gy{#b_~_36KfaG0o@@0YcQq=7kWMp#6hEPHNy18qiBTGC>;fXbkkHYGKu@iuGg z)|aFRu{ce*F9!Z|T>llBin9<{WNj!fC0Npm2 zxfsEG(O1+dwz^$Ac|K#p>O^Accek8x(n8J<)*b@PQ)YpRdRKc*h(YX z(yExCwoOw&&6lPjp5w6FO@K`Guc1)Aw`Dto%}IZQu(sYssYoZ}5xhK>q#Pyc) z&qFVRhxC3M&c4BK-j)OKQO+c)Hr-zt8QU(%ZB`!4VoYLy!Q7YeGUiIa&in2>62VbU)eHgVY%7Z zJ`xHmJ;JT$S%K{FiL^2}n%|D+(YCpE-R;f}lWuLi$A;_-UW(~Z7bo%<%LBUFfAXdE zY>(w_3K?3narCg2sEUgWAz^{0GfcXmoGJ}WIevNnwjnJ#@;I-`$<(f%m_ParF^g3t z6|>)e+}RydYZ|n_t8#Q?q{Mi5CNz5qe6$+Ve};JXPU;;s<5o&8oD(x7SSGIQX{!Q65Q+H@Ujbe z%A_Lk{7P$aZ2ZM-iDNipk#}#HLe=f`takdmaYK zi@GBF&9&qI>~#$VIb-K54N9B1S=a(;Z$BX3J5C){*~|j2mmY|}d<0BwHdz+uo5IfM z8H^MacZnLQ%G-8_QyUGSIQGPR3qbv|=>b6S1R6m*rdnMlZ+y(-Qr9eW=OSCX8awRj z^?ng(`^(54p6}Fa)2-?!Px{@xhw;5h0)JPDqdKPmg0NXKd>s2pM0~Y`>q&t~i*p~E zZ?%drzKN41d$Xd>F>L!Hvj_s7(L>!PYvT64h57;-WK{5ZBjGMToUy5-3N^Smf86T> z;EVaxRjIyRK9xIm!ghW4Ge{_an=4VRKa8y{yswx6eB|3@VRQi$u5>y(;$sTVA*Qma z^gAric_BGKPX(Pi$89k1A5CrkL1@*aN+50ZSA$k$fnVG2yS6y)N2kbS-89#jg(>^( z-%_dg8{hhKG7EuocXGMrv{wfR80fN+1YOqa*7~T|gEx$^H9b^zu1P?wOF=0#Q<3gd zA3SU#OTalQVMmc2w&8M}&9M^wxarAy+7sM$JV5?0zyF5o&+Z1nhLTKo#nN3Wmd*90dCH-bP5BvY$l!;UeI?wF5T5T*P0 zMz|_qt2`8>!i35Ov4A^*bJv;N7h?j>ujvzCe1C^u4d28Mb0rsK!_Fld?;TF9_+Y4G z5zB-03e~J#AFnV%JO;!HEY!eWfUhBh_S>Cq(|d8U-3ySgeWFy9$T-q+bG*z*j;W4N z;r>?Ix8cFjYp)D(^L+5aDI?tVIfr_m5%mFDr)qS*h&d-tX+D;c&vf5O(H!(%UR(Fo zk}D^^j*6@WP0?Q)%{~e{ahi2m!3)jzBcKpK(M(8%9chFjS%O0(l`RqSvfvtRLIuwJ zf0H|e=J8CrPm}la!|U5WFw%e%E1durHU)4)1>M+QXjc9m$=eV;gL*Kyzyg zJ0=`qN*bP0CG~?qcQfe#sluN*wB4vSw`<;Ns|j~*7w}jaSnO|9Thzit_oIK8nLn>b zKA-yuag)cUxBx5)5zwiUjJ|)q$?XB;knRsYQq&c1Cy)MlU#~+56C)!7icyAre&E;s zq2t%gbdlFv8Vp2iv5;zNbi6lvEk^ipq42nxZk+gZ7qmG`IB?QrdLzbqBS@DNMg4_mU3bBO7? z6~(76AbbH=dA-=^HM0{Y!SY361oX9jRn3`oo%He}xTh~zDVuC+VQ1BHx12#zy7ES< zmSFT*e9p_o_+e0F&|j>272 z{>nvY)hHw$s$NCE1 zD{C8_S)gZc)-Y8Yoi9)fEO(odFx^^k8m4nC)jYPn^K>Glm7!_fq)7J} z(V=r)GqMs*BV})(@Y_9H>sV12NK+L0vQfe^NyScaGTH}_p_J((r(~b~TLmkaI1I7A zh_j64)amLFK7DKU?{iar^ptMP+-#j&&gHgd6qsQwlC*rCH07tkf0gt#cX&n$s^mme z2uU6VK`GN=$KDH$Tn1EmNxC2bs)BQe6t9yLu+;vOw40Sbs@cs<{hoB?21}B55g(nt zQ=)pcgy-ZHFLr<*&Vs~Pz&Q49bWMAGIi=~YyJQwHOjSO)>dhcp0v;Qw_a6s4s2}!~d=3sAKOWVjB>!xmGvxrkJU^g1sO)ZihP^ zma#fo)hSliSWf@x|1RhjwZZlG)jyuKHJ=^Ir0!@qO0Q^EKum;z4D-C_7dZBGUIzqq zN6Ds;R#9-+SIeHF%A%$!s`kMPcz65Mnp!IBCE6@^BjEov>X=^UvZl0VqcEyTxuPEN zCmqa!ywfv!AG@7hBJk)*`st*P?qn@o0Og|#2KjWNFm7?nZ*{T|F1OGNXc+5wsKr~G z=iF)`>U_bpIA4GEDX@IZtzNMjv(0f? zL8-kH7Yz9oKAko>0rnK{<>oScOD`wI#rX@K4*{B}MNJ#u&CTw=&*A?@TT+-E#wKiH z;J`K#vMg@ilk2?!W(@QN3nnZqWt-cpzmmK%V0k|O2Tx||q!mcGF{Js4zVs!h(}_(y zmF-S?k$K&ZA)(t2?&%C6eo5047kX4Mx9BiR-h9b-mS?V9>AO0<9Ip;PiDutJ)B9D{ z_GLEc`)?O@E_!IF2XA`z;b9K0iHgbwD0_$k4U{riaq_TcomxYZvXrErE&hP!4BL}sZROd~r^pRMcugvo& zt&(JeCN+&3;TAnr%5mb0O#?}sxuqmD-p}*cAoz^<|A;dT-+Ka3{+QYo<-4_9aDhb{ zq8i@0DVvwwLEL%ZmrM(w9+?c|>Q<2VHr zM;4BHHb}ZD|01bpdC?^D?}^?blyGpj^Bk;SXMSaQFUn*fa$=}d9qBqOj zeEi@(H!J}Vx|r4Ep4X42yOL}><2uPpqN)48Vrys^8DhUxe>u zlE|4Ro0&bo$Iz>G_Mt?E5&I!_FNXghb}?ioc1#I+-vfV&ysCJOgMnP=_5<%skK>|p z7XaE#9sev)go9+5&0TTVu7d0M<}(25)vGYF`GNW(cUf6}Z!65TibeQ{lc<)eHgJ$p zp9(M-eIL`phYKWdLLVr|FhvorI7!lSNLqePUKmIrw7$wxF@PxKL}&85A74&={H~8i z$99e2OgJUHm{Y?VJdrj)dEtDws!o&p$?x~1-X4d=te+iSGxu~Lj zb0RCHwZGUc^<-fFgTqx@0>F+WY3Z>0b%|E6el@m9=F{X2CNI*jhI=fj_Uy?#Qa0&e zo3L`TU8diBpPnGv&@))E&$t*4(Lb6yXB`a=?7mtpsc0Kg12>2T-c}>nSDjwZBNsSr>7$h?J z8QebgL}Y%0$+cJv-QOi0x??oYY)UPqKM8UiAVEh-%3q;zdw(X|e5IjUZN$3!HVp#$ zxRAl34n0^yW$3grYIMEfLAlk<))`?h*SK0#7$Z>&9K|S3*J;cGSk7PVG@9f>^Ky(p zXUJ>e;Kt8RAi#QC)c4++!}C0u&oj08z~NS9p1nwW^r&h5a4a-Jmxd77K?;@q(~CqCkjn%lqMPL-9@$bQ24}($U(_{hl9GW9_ILcMxWoq z^C(k}EB)Yj-1HW~+!A2HtV*V6ucMc&uiuMNr6+mgrXvWvM`Qpd#@Q8>=I%q})$y)YZr{rwECJSsG)vPQwZnsz*0ub?k zGCbMnV?kqaH#Ogf))>_2wVy)yy|qWqqkzl=qK(+KWix7*AueXOv^$?KN&6m*q7SJX z>S%vU2xrhW3a6}BSRIkJmN!k!oh4T#J4>E6X{1v6A;wtv&@w`#xPw7{F8WJ`9 z4Kyo#|0eG>wa%*X&O<={#a6;(DPtv@# zO@#vB+7fQxM;+~n&?1>;qC1w~G&0wzSl4VnDec9F~xt(J}$?dlje zO~BO%=+Sac{jk4O0Y5~5pBV5re`(h;A!J@j1K_<`x{dgT?SGg|CsGmTO|W88#OCnd zY8q!tdyEW$!i7(QivGB~3#0L%`Zmo2aRqX$1Vo3*E;=B;=pbLN89=ipL3r9PlVl@N zIYsn6G70}UcvQkntuOV``8`)Ql?!RoZQKGI(%sOuU3UPl4+Q^NDC!Vk^I3qz!hN{g z@Pni8b+q$-g5Sg}XHetB7f)eUy6$~f5D{`0az5cODUubi%2 zSPv>nU{uYgnc`{>-b+qI|L)%)#O-+xk)i3&P8D=OX4ez3Pd|i(4FRxWA#+ydoP74@ zdlUbYUwno)@J9Zdv_6_*IkFBP_Z#U;^fE*@Iox8UB}~xVR}vcI_A>nrf3nOY$6`M$ zl3+6f;atzvgN*~0q63hK;hU;dY0_-(>@*&GJ5;wsl%v>q_Zb|=>XpW@1WT&aOfy0u z5vDTf{9ZHj%<%f$M~f#%^vzMQnAh4POMy~Z*FrAxVxkMYuAUZ`zFq`{y-WG-;wWTo zxAcouR1nGbh4JHwwY(7R_Gvi!^^bv`LSo`5cIhtps4}02jdyEs;1{$<8^N!CTJI+@a(%45NoESW$B-#7tTcYSorP(_A#v*9bv4ftM8f zw-E5xIqEMH+)dQzKApN~WQ8$8ipOD$y~73p3ib+Ha+1=R?IEB>mpgstgJ^5JM!apm zB7^ntxNxy%DF57Bz;FuU&|pE%R@D724EO&L8G^3Rzx>z+8eRl_9y$q0$Z+_@94s5sT^8X0{v*uviuA7-O*Z4Dd z41>L|Dym&MbEsqsV+hJ}lk)WGeyeq?*Yveii6-6qWV`ck@fQx_Bo^FfXqyR>kW>ti zzo2Tm$*3EImg`tF>f#KG#W}iNxdT~X8HFD`VWRpC6hl;>i1G<)ae%jwse48oxyLkW z&o=UVQYF>aJpYXLktm5@CE0MG57GnB8rLQ|!PC6`k71EriQDtnZ*f^5Le`lmX|bUG zs8j`M`7b;i=rBT6ADl~8Qf@SDW_0anC%qLg;7fd#4vkc0})#$3>UFkcMybA zeI!osc?-K|y$MEa!SJt!JXm{B$lC?SISRwpxDf?)%K5EqkTW z`rCh@b1fPNNLDohNLOf^>KUu-ij1z7>;EZDU#8Jn{z3id3 zp1qjkjm4)v7}z|Y=H=~Tb>UV2JbS&XZtY@A_cZYyYujKhp-?8lbd|a8;6{O4N_r_m zzE0)Nj<+%i%7)-$)m%giMf^ksDg^Bst{J$%`317k@ukI@MQ5(zHjzxIXC XrJNsHnTG=pQeg0O^>bP0l+XkK^)z{o literal 0 HcmV?d00001 diff --git a/plugins/deep-link/examples/app/src-tauri/gen/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png b/plugins/deep-link/examples/app/src-tauri/gen/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png new file mode 100644 index 0000000000000000000000000000000000000000..3f8a57f38e0273699c8cdd1012042bffdc38abce GIT binary patch literal 40510 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

C z(CX6fIh8mroxe4Fy?l{bj%8R307X*fDQ#(v@y+x%4>XY?ax zC^p*eaCfrcq#aa-hT`A*7=V1wKdlx2BoX`y?{jb4s1j@)b7-etD6E@cKXx@8e&Vf zWehdcr8jm&_uNAX*$0NY%B&gn)>)@jQLcO5F3UZEr zds`B64=#T7qE6myk{5rNg+99@E{Zf&GEI$b?&PagH-fv#5k*V6EEmp!A*tE}RWk_SsM5#wN>{l7X5DBVl)bv~bNp*#NXY*I)h$Ng literal 0 HcmV?d00001 diff --git a/plugins/deep-link/examples/app/src-tauri/gen/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/plugins/deep-link/examples/app/src-tauri/gen/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png new file mode 100644 index 0000000000000000000000000000000000000000..b18bceb64d257ed31d50e65d7f2b98abcfb21158 GIT binary patch literal 16751 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 XrJNsHnTG=pQeg0O^>bP0l+XkK^)z{o literal 0 HcmV?d00001 diff --git a/plugins/deep-link/examples/app/src-tauri/gen/android/app/src/main/res/values-night/themes.xml b/plugins/deep-link/examples/app/src-tauri/gen/android/app/src/main/res/values-night/themes.xml new file mode 100644 index 0000000..2777e73 --- /dev/null +++ b/plugins/deep-link/examples/app/src-tauri/gen/android/app/src/main/res/values-night/themes.xml @@ -0,0 +1,6 @@ + + + + diff --git a/plugins/deep-link/examples/app/src-tauri/gen/android/app/src/main/res/values/colors.xml b/plugins/deep-link/examples/app/src-tauri/gen/android/app/src/main/res/values/colors.xml new file mode 100644 index 0000000..f8c6127 --- /dev/null +++ b/plugins/deep-link/examples/app/src-tauri/gen/android/app/src/main/res/values/colors.xml @@ -0,0 +1,10 @@ + + + #FFBB86FC + #FF6200EE + #FF3700B3 + #FF03DAC5 + #FF018786 + #FF000000 + #FFFFFFFF + \ No newline at end of file diff --git a/plugins/deep-link/examples/app/src-tauri/gen/android/app/src/main/res/values/strings.xml b/plugins/deep-link/examples/app/src-tauri/gen/android/app/src/main/res/values/strings.xml new file mode 100644 index 0000000..5c31c3f --- /dev/null +++ b/plugins/deep-link/examples/app/src-tauri/gen/android/app/src/main/res/values/strings.xml @@ -0,0 +1,4 @@ + + deep-link-example + deep-link-example + \ No newline at end of file diff --git a/plugins/deep-link/examples/app/src-tauri/gen/android/app/src/main/res/values/themes.xml b/plugins/deep-link/examples/app/src-tauri/gen/android/app/src/main/res/values/themes.xml new file mode 100644 index 0000000..2777e73 --- /dev/null +++ b/plugins/deep-link/examples/app/src-tauri/gen/android/app/src/main/res/values/themes.xml @@ -0,0 +1,6 @@ + + + + diff --git a/plugins/deep-link/examples/app/src-tauri/gen/android/app/src/main/res/xml/file_paths.xml b/plugins/deep-link/examples/app/src-tauri/gen/android/app/src/main/res/xml/file_paths.xml new file mode 100644 index 0000000..782d63b --- /dev/null +++ b/plugins/deep-link/examples/app/src-tauri/gen/android/app/src/main/res/xml/file_paths.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/plugins/deep-link/examples/app/src-tauri/gen/android/build.gradle.kts b/plugins/deep-link/examples/app/src-tauri/gen/android/build.gradle.kts new file mode 100644 index 0000000..607240b --- /dev/null +++ b/plugins/deep-link/examples/app/src-tauri/gen/android/build.gradle.kts @@ -0,0 +1,22 @@ +buildscript { + repositories { + google() + mavenCentral() + } + dependencies { + classpath("com.android.tools.build:gradle:8.11.0") + classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.9.25") + } +} + +allprojects { + repositories { + google() + mavenCentral() + } +} + +tasks.register("clean").configure { + delete("build") +} + diff --git a/plugins/deep-link/examples/app/src-tauri/gen/android/buildSrc/build.gradle.kts b/plugins/deep-link/examples/app/src-tauri/gen/android/buildSrc/build.gradle.kts new file mode 100644 index 0000000..5c55bba --- /dev/null +++ b/plugins/deep-link/examples/app/src-tauri/gen/android/buildSrc/build.gradle.kts @@ -0,0 +1,23 @@ +plugins { + `kotlin-dsl` +} + +gradlePlugin { + plugins { + create("pluginsForCoolKids") { + id = "rust" + implementationClass = "RustPlugin" + } + } +} + +repositories { + google() + mavenCentral() +} + +dependencies { + compileOnly(gradleApi()) + implementation("com.android.tools.build:gradle:8.11.0") +} + diff --git a/plugins/deep-link/examples/app/src-tauri/gen/android/buildSrc/src/main/java/com/tauri/deep_link_example/kotlin/BuildTask.kt b/plugins/deep-link/examples/app/src-tauri/gen/android/buildSrc/src/main/java/com/tauri/deep_link_example/kotlin/BuildTask.kt new file mode 100644 index 0000000..f987482 --- /dev/null +++ b/plugins/deep-link/examples/app/src-tauri/gen/android/buildSrc/src/main/java/com/tauri/deep_link_example/kotlin/BuildTask.kt @@ -0,0 +1,52 @@ +import java.io.File +import org.apache.tools.ant.taskdefs.condition.Os +import org.gradle.api.DefaultTask +import org.gradle.api.GradleException +import org.gradle.api.logging.LogLevel +import org.gradle.api.tasks.Input +import org.gradle.api.tasks.TaskAction + +open class BuildTask : DefaultTask() { + @Input + var rootDirRel: String? = null + @Input + var target: String? = null + @Input + var release: Boolean? = null + + @TaskAction + fun assemble() { + val executable = """pnpm"""; + try { + runTauriCli(executable) + } catch (e: Exception) { + if (Os.isFamily(Os.FAMILY_WINDOWS)) { + runTauriCli("$executable.cmd") + } else { + throw e; + } + } + } + + fun runTauriCli(executable: String) { + val rootDirRel = rootDirRel ?: throw GradleException("rootDirRel cannot be null") + val target = target ?: throw GradleException("target cannot be null") + val release = release ?: throw GradleException("release cannot be null") + val args = listOf("tauri", "android", "android-studio-script"); + + project.exec { + workingDir(File(project.projectDir, rootDirRel)) + executable(executable) + args(args) + if (project.logger.isEnabled(LogLevel.DEBUG)) { + args("-vv") + } else if (project.logger.isEnabled(LogLevel.INFO)) { + args("-v") + } + if (release) { + args("--release") + } + args(listOf("--target", target)) + }.assertNormalExitValue() + } +} \ No newline at end of file diff --git a/plugins/deep-link/examples/app/src-tauri/gen/android/buildSrc/src/main/java/com/tauri/deep_link_example/kotlin/RustPlugin.kt b/plugins/deep-link/examples/app/src-tauri/gen/android/buildSrc/src/main/java/com/tauri/deep_link_example/kotlin/RustPlugin.kt new file mode 100644 index 0000000..4aa7fca --- /dev/null +++ b/plugins/deep-link/examples/app/src-tauri/gen/android/buildSrc/src/main/java/com/tauri/deep_link_example/kotlin/RustPlugin.kt @@ -0,0 +1,85 @@ +import com.android.build.api.dsl.ApplicationExtension +import org.gradle.api.DefaultTask +import org.gradle.api.Plugin +import org.gradle.api.Project +import org.gradle.kotlin.dsl.configure +import org.gradle.kotlin.dsl.get + +const val TASK_GROUP = "rust" + +open class Config { + lateinit var rootDirRel: String +} + +open class RustPlugin : Plugin { + private lateinit var config: Config + + override fun apply(project: Project) = with(project) { + config = extensions.create("rust", Config::class.java) + + val defaultAbiList = listOf("arm64-v8a", "armeabi-v7a", "x86", "x86_64"); + val abiList = (findProperty("abiList") as? String)?.split(',') ?: defaultAbiList + + val defaultArchList = listOf("arm64", "arm", "x86", "x86_64"); + val archList = (findProperty("archList") as? String)?.split(',') ?: defaultArchList + + val targetsList = (findProperty("targetList") as? String)?.split(',') ?: listOf("aarch64", "armv7", "i686", "x86_64") + + extensions.configure { + @Suppress("UnstableApiUsage") + flavorDimensions.add("abi") + productFlavors { + create("universal") { + dimension = "abi" + ndk { + abiFilters += abiList + } + } + defaultArchList.forEachIndexed { index, arch -> + create(arch) { + dimension = "abi" + ndk { + abiFilters.add(defaultAbiList[index]) + } + } + } + } + } + + afterEvaluate { + for (profile in listOf("debug", "release")) { + val profileCapitalized = profile.replaceFirstChar { it.uppercase() } + val buildTask = tasks.maybeCreate( + "rustBuildUniversal$profileCapitalized", + DefaultTask::class.java + ).apply { + group = TASK_GROUP + description = "Build dynamic library in $profile mode for all targets" + } + + tasks["mergeUniversal${profileCapitalized}JniLibFolders"].dependsOn(buildTask) + + for (targetPair in targetsList.withIndex()) { + val targetName = targetPair.value + val targetArch = archList[targetPair.index] + val targetArchCapitalized = targetArch.replaceFirstChar { it.uppercase() } + val targetBuildTask = project.tasks.maybeCreate( + "rustBuild$targetArchCapitalized$profileCapitalized", + BuildTask::class.java + ).apply { + group = TASK_GROUP + description = "Build dynamic library in $profile mode for $targetArch" + rootDirRel = config.rootDirRel + target = targetName + release = profile == "release" + } + + buildTask.dependsOn(targetBuildTask) + tasks["merge$targetArchCapitalized${profileCapitalized}JniLibFolders"].dependsOn( + targetBuildTask + ) + } + } + } + } +} \ No newline at end of file diff --git a/plugins/deep-link/examples/app/src-tauri/gen/android/gradle.properties b/plugins/deep-link/examples/app/src-tauri/gen/android/gradle.properties new file mode 100644 index 0000000..2a7ec69 --- /dev/null +++ b/plugins/deep-link/examples/app/src-tauri/gen/android/gradle.properties @@ -0,0 +1,24 @@ +# Project-wide Gradle settings. +# IDE (e.g. Android Studio) users: +# Gradle settings configured through the IDE *will override* +# any settings specified in this file. +# For more details on how to configure your build environment visit +# http://www.gradle.org/docs/current/userguide/build_environment.html +# Specifies the JVM arguments used for the daemon process. +# The setting is particularly useful for tweaking memory settings. +org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 +# When configured, Gradle will run in incubating parallel mode. +# This option should only be used with decoupled projects. More details, visit +# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects +# org.gradle.parallel=true +# AndroidX package structure to make it clearer which packages are bundled with the +# Android operating system, and which are packaged with your app"s APK +# https://developer.android.com/topic/libraries/support-library/androidx-rn +android.useAndroidX=true +# Kotlin code style for this project: "official" or "obsolete": +kotlin.code.style=official +# Enables namespacing of each library's R class so that its R class includes only the +# resources declared in the library itself and none from the library's dependencies, +# thereby reducing the size of the R class for that library +android.nonTransitiveRClass=true +android.nonFinalResIds=false \ No newline at end of file diff --git a/plugins/deep-link/examples/app/src-tauri/gen/android/gradle/wrapper/gradle-wrapper.jar b/plugins/deep-link/examples/app/src-tauri/gen/android/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..e708b1c023ec8b20f512888fe07c5bd3ff77bb8f GIT binary patch literal 59203 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

+ +
+
+

{selected.label}

+
+
+ +
+
+
+ +
+
+
+

Console

+ +
+
+ {#each $messages as messageHtml} + {@html messageHtml} + {/each} +
+
+
+
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

K7UpUK$>l@ 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 E2O(~oYXATM literal 0 HcmV?d00001 diff --git a/plugins/deep-link/examples/app/src-tauri/gen/android/gradle/wrapper/gradle-wrapper.properties b/plugins/deep-link/examples/app/src-tauri/gen/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..c5f9a53 --- /dev/null +++ b/plugins/deep-link/examples/app/src-tauri/gen/android/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Tue May 10 19:22:52 CST 2022 +distributionBase=GRADLE_USER_HOME +distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.3-bin.zip +distributionPath=wrapper/dists +zipStorePath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME diff --git a/plugins/deep-link/examples/app/src-tauri/gen/android/gradlew b/plugins/deep-link/examples/app/src-tauri/gen/android/gradlew new file mode 100755 index 0000000..4f906e0 --- /dev/null +++ b/plugins/deep-link/examples/app/src-tauri/gen/android/gradlew @@ -0,0 +1,185 @@ +#!/usr/bin/env sh + +# +# Copyright 2015 the original author or authors. +# +# 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 +# +# https://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. +# + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin or MSYS, switch paths to Windows format before running java +if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=`expr $i + 1` + done + case $i in + 0) set -- ;; + 1) set -- "$args0" ;; + 2) set -- "$args0" "$args1" ;; + 3) set -- "$args0" "$args1" "$args2" ;; + 4) set -- "$args0" "$args1" "$args2" "$args3" ;; + 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=`save "$@"` + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +exec "$JAVACMD" "$@" diff --git a/plugins/deep-link/examples/app/src-tauri/gen/android/gradlew.bat b/plugins/deep-link/examples/app/src-tauri/gen/android/gradlew.bat new file mode 100644 index 0000000..107acd3 --- /dev/null +++ b/plugins/deep-link/examples/app/src-tauri/gen/android/gradlew.bat @@ -0,0 +1,89 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/plugins/deep-link/examples/app/src-tauri/gen/android/settings.gradle b/plugins/deep-link/examples/app/src-tauri/gen/android/settings.gradle new file mode 100644 index 0000000..3939116 --- /dev/null +++ b/plugins/deep-link/examples/app/src-tauri/gen/android/settings.gradle @@ -0,0 +1,3 @@ +include ':app' + +apply from: 'tauri.settings.gradle' diff --git a/plugins/deep-link/examples/app/src-tauri/gen/apple/.gitignore b/plugins/deep-link/examples/app/src-tauri/gen/apple/.gitignore new file mode 100644 index 0000000..6726e2f --- /dev/null +++ b/plugins/deep-link/examples/app/src-tauri/gen/apple/.gitignore @@ -0,0 +1,3 @@ +xcuserdata/ +build/ +Externals/ diff --git a/plugins/deep-link/examples/app/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-20x20@1x.png b/plugins/deep-link/examples/app/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-20x20@1x.png new file mode 100644 index 0000000000000000000000000000000000000000..a6ac2a8ccf0aff8558f948fbc2c9befd5aa34361 GIT binary patch literal 1036 zcmZ{jdrVt(6vux^=qiX3s2gLFAdJBT+`Ui-HZI6Ug^odBfSZdL7%)L)4%X?Ib07hA zS>`ZBnHwl(Atsws7r|Mcqr4pBu@3Bm(Lu|r&5C7>4eS1K87N#!3gJRqnirZfRZ3AHJyk7{dUZAP~mklTt{3QZIC&moJ2BpI4 z)7T&diw1YU1Vb4vUHR;=0r6vgtwj2|TL9ZCk}tdQOj`ZqCG5=P*OVy-a+E z0NS`jJA(%&MRKhJpwnzwu+yNuilxtk^3%T6$4~btYG;k7ru5!2{I_ zixcC7vFV_}$r&4c+iWkLwkRLle>bdq_9*w=pLuAd?F>_{SwdaoZ~v|RN@sjVrrb85 zKBk(#pf!|h^fAL4_dAO0iCLNfsfpSz*KY1p`}8Vf?rARRj10iYy44Awro;N#GrJv4 zNlHXtdITo3#U`;RRk%sd@%MhpEpPC&iXDfD5)-og$Az`ohuleC^}9c&>t8++S=6pS zTBB3w%&cmHqs-6Z`huHOE!FsH?8z`UUg^26Oi6q$m&+fTp{lAHK^p;oE=WZVs2eNxl-5Q%8oxqy*O0HcGt6UDF)iYE4;Xps+u*IXq%9 zqH^f^dmsJKG@$+Y=Dto*5em5a!EnnpY4bPDEpJUG2n6j#TdU09zti+3`-tlv81*OI19yi`MyisoB?7scSXN{UY5$gE3tm}TY*YfMv zf0Ja!Cbl}%bbqMFCEN0qC`qVp!DHU6AAfG*btdI9 zw6tTtZ%Nz$oQM-luiB38gB1^n0uf)EhInd}DZ ze}Iw?3yP1G|3Bcx-M*g%!k=sq7nc;C6z7$kc)~>Uv&a|$5iKCdf0q=`nkbe*B4JwM JFMD&V{sG_UJ5T@s literal 0 HcmV?d00001 diff --git a/plugins/deep-link/examples/app/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-20x20@2x-1.png b/plugins/deep-link/examples/app/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-20x20@2x-1.png new file mode 100644 index 0000000000000000000000000000000000000000..2869541f735d2f3f890a5b400fcced58698cbac4 GIT binary patch literal 2216 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 R+5rQcJL_y$amF|Ce*nJiS(X3* literal 0 HcmV?d00001 diff --git a/plugins/deep-link/examples/app/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-29x29@2x.png b/plugins/deep-link/examples/app/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-29x29@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..a4e68c8d6d5c4a564be190fdd200d7214eda2db2 GIT binary patch literal 3394 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 R+5rQcJL_y$amF|Ce*nJiS(X3* literal 0 HcmV?d00001 diff --git a/plugins/deep-link/examples/app/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-29x29@3x.png b/plugins/deep-link/examples/app/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-29x29@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..e4adcbcedc3780375b853ef315244b28a735f047 GIT binary patch literal 5228 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 W{J%q-7Pk)mF@QP-t6ZsQ9q~WT1(vM< literal 0 HcmV?d00001 diff --git a/plugins/deep-link/examples/app/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-40x40@1x.png b/plugins/deep-link/examples/app/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-40x40@1x.png new file mode 100644 index 0000000000000000000000000000000000000000..2869541f735d2f3f890a5b400fcced58698cbac4 GIT binary patch literal 2216 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 literal 0 HcmV?d00001 diff --git a/plugins/deep-link/examples/app/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-512@2x.png b/plugins/deep-link/examples/app/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-512@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..704c929133f23779ee37099bf32d400b01599be1 GIT binary patch literal 121323 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

>qIa)CJDQwHr3JKGv}<_ybIUd;~Q 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 literal 0 HcmV?d00001 diff --git a/plugins/deep-link/examples/app/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-60x60@3x.png b/plugins/deep-link/examples/app/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-60x60@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..2a9fbc26ef1ef34cd7b0e4413f40d7912e3a777b GIT binary patch literal 11091 zcmbVybxa&i@bBU7Qd|!!c0fxxoWr3&ixeyFR@_~RbGW;^I}~?!FYfNe;qlE&e#v{u z`{!k|JIT&|GLzZtWOimkzR62sppu{h000bWDKW)&TKC^XhJ9aaDx;0w36+7Aq8tF= zMh^h^1ONb!?^`|x0DvP1065YG0CEe)YCQXr=;VVbIH+K`|8ZkuuGM(dS~w1NB-A%9(ww`^<$_y6yg6q3Sqqi{J^I0gh-DFTX=JGs~f5J2uvB@||nHA})r;MF1 zQ%r`Qm5gZENv*k<#iRIt4NfUDv1GmEq(3Ws{hKAzIAjweCG3KYDU3BE@d1vvAfW~U zsmc+pcol>G4!x{Z4I}Mb3Z)D^lpk|&Dvpp4Sr%(~saU9L=^=f z;tnV$W=t2Hsc;Wb^A0)QMQ)Rij!h+t{zc7-O?b8~n)zL6UB!)%6CwJ5o`E8QUiNE* z6}Fq^=wssGrYg@+7rpq7-Nf7_qUUssznq~D{BF?2q|i`^HM6Gg30!d?i#i1|cGn|l z4#a_r`Ib?8c}K>ulzMa|Ee#zMrS6$r!19zKe<;f>q~@YR;T{oaK!(Syg851(m0s~h zCz+^SEoWbBP+Ag`o5~E}{oFCzj@1(Hp2bNu$5odU8BFup0^&V(-P_RNFr&%1eBve= zkoI#&7cNFbZNYslljzA0h>{GOy1OE#;S0cV5GnP&h$~H#t5iN9z_FW-@`o`Fe!Mx| zdTnGVx$7MAg<0ICP_v*y!BkH?_Wv+4vv%s%ti3THErQuVr3ZM+o@jj%%;E_+fji!% z_|Zxg<->L#qI4gQMSrzFZAaR|oJnRW%vm>i_g2{(J%Jhz6B0OLLFb2WIVU{V*unMr z3Kw$y$639ji+S!vA7x^K3dh4Ged`a?hBxLxUF66n4aFbMXM# zAS%6NZ+MpJSrd)=GdRsxtuf{S>Fn5FxqKV>BM~3+l3B8Qx#a=V@}Jz{i|w7$(0q3?+A=zs)S1UfDFnhhfl& zFnz2;c)}}iU^n*uh$^HK|CCoQLQjpGj*6>@T==}XP?UZiC0m+V!D9y!&erpMvWWdn z3EBNHfsU&i$V7^qE*P}Xjtw`Vn5?v0O~saBax3;JnewD(>{86z`X0!l8_vQF3io{XMyqQKF&-OQ9rj{^eG0=%Z6Itr2zO339BTdv zH0}1{(f$~*Wv3nV4;$`-L+n>g0YJ;uWvW2|ywulTVvpf!KzVTWoru+TO@DJ*luuUYCB#o%Wa0bsIM=sf{v-p;R zvg@2Z22r&&cxD0h;4J(fCwI+Pbeev-y$(Bi-$!iVb~GWmzjA-S=K#M9gvknQYB3%p zFK<{k?|VpfJMrmNGe8410i>P$7ut;tjm3ZK;bK7X-wKXRJZAZ}(<@Sc{i2a|tP#i6 z)ju_IF1i-nw)V=M`&{+Q{euBU{55?$1=#~HRSS7g6)q5oG^#8@JG1HTV|I0U1a-DTy5z%Uo?EK8#kX3w;kWHOx>4xQ zIe+h{sejhV8pV{E_;q~<)_RLMSV5krYEFY`1HurN#fnaR-hT}{$Lxt)`6NKp?bx0B z=MD~WwJkn_0Z9|>;N@*&{!SFQa!nt-V^h#_W^{!@=}eAgna~f*zgZ=WTGn>rwC1R_ zVfCA>{tJlv+soOGH@w06`O)6=%8o8tjNYd9-9chj&8h2WDLWobt+%UQ^>ZrWVu`W|$_X)acg@o<}V0cf*o(4eQEMC^;dQ_26 zeFw{vX$l`4uB;}fM(!(E;==meW``l0lvNNlw38LlDSR$$V~8d(U#1tpQq+wl^3nrc z1tFWP5OHb7Wbi-kf^J%=`Q1d^g#=kTb9G{VBGIMn1n01^@c0yS#eP>${aX^tXXq9r z|0}ULqFakO?KQv-utM4(Z9zp1s6HLL?PmWooX*PkNJLXy;{F^$%Hd&lBI9h$+eJjx zVJG%{)kt0eThfK18FU*VWp0gT;$(X!O=HhZ_eGZBlCg3=2C=W#VFnRs!PVI6{70jq zy&tGMEb{O0>W>Am&tpO0!Lr&Y#&`=+RiSIBM>*R^{%58-my=P#9EAk0Vl?NWze|_q>jKi4G5rwvH7PPX9=sM>;}?6= zQ#7Nu<_bkD=E7NBf3*-pp0x-xm_b(6gCWwm3|KI0F%B@V0)Bg8(bcjA|d9pPV1vu3?7seKYAeaM1NH8XuAx z<*th1e6m<6HLx4Af$JB6?2gD0pAG^^4ZulV4auQ$R8#k6adh=sgVXd?wtwr??{JgM zOGmy!mPr&|I$=4Wg?9}E0IHO*UNaHv#_yP)`^Lx{sC?kzmSH#RE(;@y;dWYxoe%?3 zf7tu6N}R<|`2>0~^V_BOsAM4^A#w#Uhc;@UQR~+lyT_98=m9a|5@3XJ;Yp5h3+Wcn z{~(9mt%RhS{wvJ!n*3ZlN(eytq6|l0&pN8ZI4?LMT|qDw*=v7ZRbtIE_?E(T`MKzo z@Do;`)ZmeSOO1|^e29g~p-B36^*$g0rn_3a>QQ&>O?+uR*}=(}%l3j6b)B}ODG}4- zr`$S=98Nb(^Ywg27YVm+!!kMQf=)rhbM&#X{vGJ%nVD86dz2%WBmeBM`L$xa6X2LHx7=*N;m>x&X_xk8<eFCv5)jFVj;VlO1qSa&!X8F#2MHy+s*3dqhCY$v=R`xj_&?&IqvY<*WwW^Upy9p3moCR1MthS8D8*f(a%ZD&hnWkN8^M-QKARzg1`muM znAwT(1yccdz7=~|<=l)N9=$l0i%5%jDvF&!TN?IF_=VM=JHuO3y8Q=^JD6<3VUqq` zB;9zHs{+<&Qo|96eN>Cd2)<7f`r4ho}GlJ`N`VK%kZjJg6cP^rdXrzk$u-Q#}L zHx8xLPigo$3MjBFU+@*t{vj)jxg6MVE`o-1*9(;-X^iWVWd{hcA-7=nbrsq46tcy+ z>K{@7M*D<#rX?ClX%vA|)kxl7grlcvuauMm2m3yH*i_6L1;;o4u+(S+e- zfXE4Y_i7N^LAAKy6iSWqFggO5VC-CMl`fZb!w zAh9u&)Q`)8A-fN#n}fpAE(#am?edLgPdJnLhyq5$G=N{a0?TnSfln7$eMa8@u)z*H zgZlQXn;~Im{Le#|sKIuj_bzp&UlF@N)_3>;HDMVsj8&$4@+NzGPFpx1+EZ>04pi$f zc`-Wm=&5YJOdO*+&|v?*4T%jU`~HF|zNl33XY0)0S`fr#0m_00*yoVecN#Z}j)f3! zgplSH_ipmlU}q?K3M;(Txt_|?BXWZOLg8tM}pcZeeSRVSxk3+C8&bz~M2ZSzuUs=4gaGsvV&7YO0 z9VhictU@^=M4hGXG8sItq(yBNp#~Qs&k*|amna^MvG&rLeNR+CWu%;c#&c^QX;TDRV$ zB+)68O(!o_BhRV8`ff2AOiws9H@i`eZT364{Jf~Ks_;I4y>P4N24rlQZ)uCmD$|W2n)Hwq`t}arkbLiX&}(!;b2ztpPcxBak>jOod`~R zczSghW7a1mSfaBm=?qv*UZF_SpRwg*Y2}mInQ(wSwqU~5eJY=b(NN3+1Hl9+SxPqY zLZrf5Uwg6O($syfVzzh>omB&!VaIk>EO|yb?#45huI5jFJe|qlJR%nfh9AoCXaTpm zv*XId(GiGK51uU15c4F0De~Xbr2piYsC)H|0;xc`x;!vmUfh?%^~pLDg?P{r)3g(I zR52g5J6QXJ*ceCr%|G{=bTCR7GyBD__N455O@uHzkqx#u4gKnPs!cnJQ9Yc%sW=_) zRk&?I+ImKeIDiNvDdC3fW1A51k@v7LeT+x?I08zQ?X;#}{fCjM3CF*lzGZX#S0Cwk zp&#iu(F?t@(z!@vfD7eBN)4A^YhAbD4BS0aRcBk^957?BIZ>CjqnzIC3|?vXSX}P&V39?cbdK^%r8@XhsF-ekshz@;MKlx+C@O(!@3!tu;Ey}MP>EeDg z5~xVJ#YobFp5>BS#hU1^ZIEAm#(=0 zDH>cm1j*b~3oCZgE4a#Ga5ler!pBJSU(r`eclf%Db!b zx|Dg2TFbA@*3Ac}p6dvNrIdO>DMlG#`q?xk zZd@W;jCfY5gRPg6i`pMy{y1It^qv_qzk)f8nwWQ@;4N+#poI9}H4#x^R1Ac?jy8V7 zE$tfi|Fxkx$J2yCrlyV&#tr6l+;`&*X|f=!L2$WY9*&zd&w%d=OzX(Xw(0$paZfNU zCGX)`0`|Dg=-H~S!P6A=a;%kQiCQgPYm=QE=sv4RMsgi zp(ACT8Pvf}d}4---Vr8O_8%xTFmVI3vmV4<&*e1dB|aQVkJkQZfo~wJ3aYgCyG-aI zM6lEj=7 zj{39NNVkP03XLF(CRE4jAvG?Mrq&2YCkTkCuGC&>Y-T6e9Tt=qF$B1;uQ$DR8JEa1 zXX1iX(wBD8wHIB8ne|(L_6BN(#S66m(+IPmrK(yy2o@q5&UI#uJM*tccz6TKiyk0pR}997AaD5f6#Tan~`j&I!(NiV25RiVSbDPCz!#kl(e#)y=+t{l~S@lx&3 z?Tva@cAsIV(eVnTA_R*3S7n^#ckQy&p0ge)*iSFMx&vy6SM3nS7s*~FHQ(o zLj>M8&ga2|9v3lg6~UjeB~%ipzi3wr5^Br@GvWbQEYRq6g{KjgdMPW;J9MegqSVcC zy1!E@dG942RV*-AqI6p*nCTjb`88;-21}kmUnpl9Ab^W&yS6@Y`p(A1lqiMcbJ4FH z-czthydD#;mlJY7WC=ZEYD$)@s7p|nSjs6uuLk`cdQKbZb7W_^9`vNfm+7D*Vx7?(h~ zN-%7@$!dSW2yw4)>Xt4#(f9ZP%trG%OFnBTZEedI3qD1|RCytMQ4>REwZ4lh{VRWj z2f#SpL{jQwS5QHUL;lcOk`f)iRk(YRGG*qVO#mCA%NA7yJl^ej^*mSVaC6mvY=z{D zjOQd#>mG~f(`H_Z&`lykjyd)1{UQPj9AjqzE4ZlSlmy}-32!{SE6uDXNl$rA#$`95 zj?Ll6PR2j&@u@A1omZwRONOlZHsO_xqha9y@M;!Pg4^EiL-n#%?^^nji2+=`+K-51 zOI59=UHL#QZw5*_VDhM1R(c`0B}LA<4!c?|7oQ^Z0TL~OWa)4$y*c&kQ;yj#t5K-3?afM2m z4WKc0(bQqyf?kW9fGa=JI+W5!-SMt91 zf<_&8z0E*tEDl!-$yGZ*pV!5pVU%_MOguAW(;Y(-`WW7i99T_0;BJEy6av(7#M7qNy_+P!t;hkA4#mDnUc27F&00__S zeukOzOX-)YFg12su1PRBhu8tZ)lWm254mdebGa|{pCg=9vAkSM_G#yp$NguxkyKps z^cfs55wd>+r!#V%u6aLVJ*(=7R_4DLe>IKL+os@s<%9q!X}u{822O~s>#$hnX?JtQK@ z*|-CwqXIzT3m(j)gURS7#&4}`jL`^L8G>0oXwf4tx;=CxduVEWP@x%3lPZQOM>&L^ zn+43f^UPYi+@T??&2w%)d+iLd02Xxhzy9Eude>9DG?-%R5*^13$zBiDRFn7HN<)UO z5R!Xi?GG_!);OuY5*2|BvH@VwD_5t}1*# z*%nVc0?gvcuppBLhZ>O$iX>!B={7S>%7KWF6@}1cDX^>6Vsk)J)`Hxusm-nsb1ILU zebm)j$cUWR^JfxF+*#BKoM&_;zNPL*%X+8zlzR5gNh9;IhX{yCgxh!W3gJfVg=hhZ zKfNSOP;)1Z3mFth#KXMe>Cl7aPpyR4z1i%RzQO?+Fr7v?A^`v=B53d7B}5@Z5T2UP z3M$bJn{&a8{DG`{HYp;SIMy|k{27Deo~+RNUTvtz0Kwl4zgh|t6y>;6oWc7IRb$A- zAQ1TYz5126A;(}WEkz}1tOH;S9_ZJk*+3`uGH}>+L z5Ywnv_7=f4u6tRLYZh9oW9tvd*~>p+oyqsk{P^ZPBt%BEUW3ThLv06Nu3E>u+inW- zoJuK+`#6uMfM}XA#$CC!fhxwo6I@{M=SS;7MNcha2|+zvk(!sjjOTWm9JcLaNUr~~ zscVT|Q=TAfV&o=dX?~Ra@duqI-OC%QY@~NSvrkZALi**b9B0a;o zf;^dJ0VM=L%Wa4MM&RW9lSt4@+V`R%Q29jK$0}f1t6U01zEChm70S#wom`)x>y?0- z+GeJoYvxKd?n!+K56?;s?JDOI_pQjIkq8U{raY;$nc>)WbGw>_x0~rdVx4;(%-uzR z6=?#wV=ieM#spDe`RL;L*>}A<@C^-1iRY*yIDWt8eo3u`87a8=Hp~&DSqTke;PSW5~#Qb8JwN6TM?OOJwt9*CU zB>9!ezJbB!*cqpybB|+OyQC%-LY0V0DR_{){8>vj@`JFWtAAP3IH{1(f*+r?DmykR zdSc6)Wv0Rp1?C9W!6v!;4oT(>{AyYJQLOiy0`ZRuVfAlpPtMBz(ql1l$1bXnF8X3%5uVDbJZJfnznF)D8jm6#U$7 z^s~9yYnqqN3x2Ed8?P-C7*^@(IIezIp*a!sxKRWaI`ImX>ccG@Ry6WMzQ&W zPLx*1{1HAW^_i>HXA4O7DX#gm98foe7sHl|kf|l&F+AT4(9ha381Wt4=2_FY(E)JB zzI8_P=FL}L#C=O=Z0}Qhw3E>Soyqg~uceORQ!FdD37)aJb0sw9s}$N2Vvt{DKiqr# zW7jiott)>UAo?-p_WIxwi+neH{#iZ(mVzu@T-U4}CR3=0(>ipOj{purH-DC-Ll_A! z;GqG}k8Rsk68(w2^K@b`BJ&4At6WaNx?}QWpk2{rp(NfGPUbCq#bQ)ge%1FoY#}hs z=4HU^4V`P{BJ=_j?jCU4PTW*D(<8YxwhRey@ReQVm{_0Cs|oQS)>_B+JXD0 z!};#&mRXtvJj8%DJz!?MY3ZkGen|sT|O$h~WP*Qn3SS-*Te8JvwJ=ql{U|#vR3Ha5*{UK#X(ivTJ zkfK_Lq^D|cf|BR4dFY4LEm({qU9RGOe2J8gjiL_!;76Ls&b3r)NaL^Jp!tT@n&{{I zE#*_^b1?t*Q>c`4+}kWulV1IFolb|xUA8y~F2a)SX+Gk=FfSKJ_~bwzTL%BpeW>Mm zWIoTh&T65a;Bs3C?Ul}%{Bg;W!+fiSMe}Q2RH)6d<>i-iUJrW?BGXLbP*Cpa$U+{Q zdSWAUp0`wELp*ZyPTK8@>=~~^(aRWu4{(+Rex)?%sdD)fGIefCnJk{VBMW2iQ6xpm zqmX1Mp;Sl%nuY}FaIomQpt}KE^uY@T9mIJEhAMQc*{HzgJ-p8despU@f>FydJ$djv zTWLh+|C}g(ZFZIc`S9CQY~rq*>-Qa5T5~@B4Kwv5cSo*TMCUU537MOl;7aZI)pq!w zl^x-kfoBCIhBaFo)LX)o<^9UPolKb2FrY&o3`+_f4ucI#S=3$J#{~*j562c>izq8&Fbiy`Dd(pSuB6AoL)wvV zbERA?bYR^GufDS$p^pgQ`Xt_293Zt}T*PVO&57k!&F4d%kwT^K56#U+P_nORL0B9) zRcPHkOe!dTlms7jQdwTI4kk#(rLuX85-Nj@>pdlwAG(uiWx&khb4fk8(QspMD@#-^ zhpRznrKzJiM{!>eS8r!p5U+`Ou=VnDR? zy&8iuWi#ynE3VA>Z#Lkuo0v2(R{Ny-R@R@64k=~$(0_M$_wGL_;g;LF`d#k9vp7_S z_X)XZ^eCQ+d3l4l?tb_vfE24$yO81>xjwrH7-1YbcrgB4ZfMsW;ZV3$#$)$5!6-V5 z0C;j|Alv*_<&M0q(b(>k>}UFhu?>sm3@5s7szA!#qf3bKbEBz7_drCi`X~JFA}i_A z5A1wFL9yDAJg*;ujHr-Oo|FE)EUbCo4A6Po{yPmhw_5A4-;jL4rN!5iSwWjVtLF={ zuV}47Uyw9#9rQJ($f;jQ&$_nn}Vr$%7j-lw#zzfhzY3HcsYIQ?*MBmgvL`t@Vgq{ z^DJiLPpwJ|;$vosU%-6YEbp%JQ@z_$-_+q9Wr_gVvKLeD^o^Sp<#)UxziL{j%ZSuj zd=H$D+TqcD?iBNm@U?>|VEgtF2}|D7wFcLR)2}Fz5!Vctu7~a3$U@P2sm@r7b7<_k zBNkmi42o<&u|8oEMhWJB^4DLzK@@v`~LCJ#au*Hz_ zJg${5Gb6`etB3-^5B~VOO0#OiV2f~l3?$z|n+{>-z7aA3p&{eFI5roSx*v$!i6yNV z0iL!Gr%eVzzkE2J2^0{Kra9u2rIA;YNdMmGBs^VP8=_i1A!tB_798tH=?d?bNQSd_ z`1UN^{1`Db?QCpuHwVd##-jA~ev={#HMKc$kf?^@Q3@hV$|bokS1x4^v47NU|st^|*h$)eNiG9Jzfm-`t%1 zfgVJ5iysD^XZ%>qV2 zPTD$%p^+XhRp84ZkNwY3o~c?#szd;DB$<7Z!O&kQ*Eu_KPnG%CJ{qw+VGuy|TMU(N zov_SMfhAAFW3-K1n!P?Ia}wD#M+cE=T}3&I)wNQvxfYw~{`^|MGz2QS9ldONkUq%@L@E2ii`2R$CS=NT+P8y&%ub-z|QEM z060OMpIAZMte{WIpf9|iICwvOVFrPCK_EAkw158xz{=Xt%-Hq+4M0N%%De*@|3`v@ rnU#^fgPxVm|6n*jDRXe~f;f3W9RFL0+4kx6I|d*vE-zLA(e?XZ6pSSZ literal 0 HcmV?d00001 diff --git a/plugins/deep-link/examples/app/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-76x76@1x.png b/plugins/deep-link/examples/app/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-76x76@1x.png new file mode 100644 index 0000000000000000000000000000000000000000..2cdf18485f69316a5ae921f83997baa4c0042e50 GIT binary patch literal 4515 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 literal 0 HcmV?d00001 diff --git a/plugins/deep-link/examples/app/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-76x76@2x.png b/plugins/deep-link/examples/app/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-76x76@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..4723e4b45ea6377219fcf9475c2c75ae075fed5e GIT binary patch literal 9257 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 SjxJs>07Y3fXqB{C=zjqou7cM9 literal 0 HcmV?d00001 diff --git a/plugins/deep-link/examples/app/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-83.5x83.5@2x.png b/plugins/deep-link/examples/app/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-83.5x83.5@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..f26fee45c06c140414d4de5ef4a6911edb665b3e GIT binary patch literal 10219 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 + + + + method + debugging + + diff --git a/plugins/deep-link/examples/app/src-tauri/gen/apple/LaunchScreen.storyboard b/plugins/deep-link/examples/app/src-tauri/gen/apple/LaunchScreen.storyboard new file mode 100644 index 0000000..81b5f90 --- /dev/null +++ b/plugins/deep-link/examples/app/src-tauri/gen/apple/LaunchScreen.storyboard @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/plugins/deep-link/examples/app/src-tauri/gen/apple/Podfile b/plugins/deep-link/examples/app/src-tauri/gen/apple/Podfile new file mode 100644 index 0000000..98045f1 --- /dev/null +++ b/plugins/deep-link/examples/app/src-tauri/gen/apple/Podfile @@ -0,0 +1,21 @@ +# Uncomment the next line to define a global platform for your project + +target 'deep-link-example_iOS' do +platform :ios, '13.0' + # Pods for deep-link-example_iOS +end + +target 'deep-link-example_macOS' do +platform :osx, '11.0' + # Pods for deep-link-example_macOS +end + +# Delete the deployment target for iOS and macOS, causing it to be inherited from the Podfile +post_install do |installer| + installer.pods_project.targets.each do |target| + target.build_configurations.each do |config| + config.build_settings.delete 'IPHONEOS_DEPLOYMENT_TARGET' + config.build_settings.delete 'MACOSX_DEPLOYMENT_TARGET' + end + end +end diff --git a/plugins/deep-link/examples/app/src-tauri/gen/apple/Sources/deep-link-example/bindings/bindings.h b/plugins/deep-link/examples/app/src-tauri/gen/apple/Sources/deep-link-example/bindings/bindings.h new file mode 100644 index 0000000..5152200 --- /dev/null +++ b/plugins/deep-link/examples/app/src-tauri/gen/apple/Sources/deep-link-example/bindings/bindings.h @@ -0,0 +1,8 @@ +#pragma once + +namespace ffi { + extern "C" { + void start_app(); + } +} + diff --git a/plugins/deep-link/examples/app/src-tauri/gen/apple/Sources/deep-link-example/main.mm b/plugins/deep-link/examples/app/src-tauri/gen/apple/Sources/deep-link-example/main.mm new file mode 100644 index 0000000..7793a9d --- /dev/null +++ b/plugins/deep-link/examples/app/src-tauri/gen/apple/Sources/deep-link-example/main.mm @@ -0,0 +1,6 @@ +#include "bindings/bindings.h" + +int main(int argc, char * argv[]) { + ffi::start_app(); + return 0; +} diff --git a/plugins/deep-link/examples/app/src-tauri/gen/apple/deep-link-example.xcodeproj/project.pbxproj b/plugins/deep-link/examples/app/src-tauri/gen/apple/deep-link-example.xcodeproj/project.pbxproj new file mode 100644 index 0000000..4c01a95 --- /dev/null +++ b/plugins/deep-link/examples/app/src-tauri/gen/apple/deep-link-example.xcodeproj/project.pbxproj @@ -0,0 +1,479 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 63; + objects = { + +/* Begin PBXBuildFile section */ + 017AE826151E36372534A964 /* Metal.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = ED2B1BC06DFE0498ECDEEE51 /* Metal.framework */; }; + 65A8D948440EDAA7F62BA1F4 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 486CAFD81CB14F9A2DF72FDF /* Assets.xcassets */; }; + 8267407118D9FF73C271D81B /* QuartzCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9435FC7E183EA6260CE76637 /* QuartzCore.framework */; }; + 8DD47883B792C4FC20927AB4 /* Security.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4BDECB1ED2EEEB5A6A8B8372 /* Security.framework */; }; + ABA8D5D86E66C92292DA3B3E /* UIKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 2F316D1CD78DD2E070DA5C17 /* UIKit.framework */; }; + BC36958BBBA7FE61066213D7 /* MetalKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1C21C8B4A18EC7D0B5808C10 /* MetalKit.framework */; }; + C384FB77F116B05F8E642CA8 /* assets in Resources */ = {isa = PBXBuildFile; fileRef = BF7ECB9AB55B71692A21D5F7 /* assets */; }; + D01EC573029B7BEC701F6012 /* WebKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6BDF5DBBA740DA7D86791DEC /* WebKit.framework */; }; + D4D232DBB85C5C1594FACC3D /* main.mm in Sources */ = {isa = PBXBuildFile; fileRef = D99665C1C3247732C6BF25F4 /* main.mm */; }; + D7A9EBD47413746EDE96BDF8 /* CoreGraphics.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B18865218362A4BE07527DBD /* CoreGraphics.framework */; }; + E26F7FA923DA1EABEE42B63A /* libapp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 403FB4BAE59F74EE98EF1EC6 /* libapp.a */; }; + E6992F2651B864B15ED14925 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 1846ADCEDC2C208E1037ADC6 /* LaunchScreen.storyboard */; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + 1846ADCEDC2C208E1037ADC6 /* LaunchScreen.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = LaunchScreen.storyboard; sourceTree = ""; }; + 1C21C8B4A18EC7D0B5808C10 /* MetalKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = MetalKit.framework; path = System/Library/Frameworks/MetalKit.framework; sourceTree = SDKROOT; }; + 1CAAFA750FD735A285DC1238 /* deep-link-example.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "deep-link-example.app"; sourceTree = BUILT_PRODUCTS_DIR; }; + 2F316D1CD78DD2E070DA5C17 /* UIKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = UIKit.framework; path = System/Library/Frameworks/UIKit.framework; sourceTree = SDKROOT; }; + 3DD32303BEC377C10162CF69 /* bindings.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = bindings.h; sourceTree = ""; }; + 403FB4BAE59F74EE98EF1EC6 /* libapp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libapp.a; sourceTree = ""; }; + 486CAFD81CB14F9A2DF72FDF /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 4A33212233BFAA738F6A46FC /* lib.rs */ = {isa = PBXFileReference; lastKnownFileType = text; path = lib.rs; sourceTree = ""; }; + 4BDECB1ED2EEEB5A6A8B8372 /* Security.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Security.framework; path = System/Library/Frameworks/Security.framework; sourceTree = SDKROOT; }; + 6BDF5DBBA740DA7D86791DEC /* WebKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = WebKit.framework; path = System/Library/Frameworks/WebKit.framework; sourceTree = SDKROOT; }; + 8AB0099573FE8BF1DC82CDBA /* deep-link-example_iOS.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = "deep-link-example_iOS.entitlements"; sourceTree = ""; }; + 9435FC7E183EA6260CE76637 /* QuartzCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = QuartzCore.framework; path = System/Library/Frameworks/QuartzCore.framework; sourceTree = SDKROOT; }; + AEA78299D25FEC31E2988090 /* main.rs */ = {isa = PBXFileReference; lastKnownFileType = text; path = main.rs; sourceTree = ""; }; + B005488D1B56B657AB52E28C /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = ""; }; + B18865218362A4BE07527DBD /* CoreGraphics.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreGraphics.framework; path = System/Library/Frameworks/CoreGraphics.framework; sourceTree = SDKROOT; }; + BF7ECB9AB55B71692A21D5F7 /* assets */ = {isa = PBXFileReference; lastKnownFileType = folder; path = assets; sourceTree = SOURCE_ROOT; }; + D99665C1C3247732C6BF25F4 /* main.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = main.mm; sourceTree = ""; }; + ED2B1BC06DFE0498ECDEEE51 /* Metal.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Metal.framework; path = System/Library/Frameworks/Metal.framework; sourceTree = SDKROOT; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 5C340AB143FB1483D2013F8B /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + E26F7FA923DA1EABEE42B63A /* libapp.a in Frameworks */, + D7A9EBD47413746EDE96BDF8 /* CoreGraphics.framework in Frameworks */, + 017AE826151E36372534A964 /* Metal.framework in Frameworks */, + BC36958BBBA7FE61066213D7 /* MetalKit.framework in Frameworks */, + 8267407118D9FF73C271D81B /* QuartzCore.framework in Frameworks */, + 8DD47883B792C4FC20927AB4 /* Security.framework in Frameworks */, + ABA8D5D86E66C92292DA3B3E /* UIKit.framework in Frameworks */, + D01EC573029B7BEC701F6012 /* WebKit.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 146BAF1D709F8A0FE5B07709 /* Sources */ = { + isa = PBXGroup; + children = ( + 15AD0F4F98577F71B7752D85 /* deep-link-example */, + ); + path = Sources; + sourceTree = ""; + }; + 15AD0F4F98577F71B7752D85 /* deep-link-example */ = { + isa = PBXGroup; + children = ( + D99665C1C3247732C6BF25F4 /* main.mm */, + 9FE22F548D05F1B0C03527E4 /* bindings */, + ); + path = "deep-link-example"; + sourceTree = ""; + }; + 1DC58B1705AA3ECC6B887FE7 = { + isa = PBXGroup; + children = ( + BF7ECB9AB55B71692A21D5F7 /* assets */, + 486CAFD81CB14F9A2DF72FDF /* Assets.xcassets */, + 1846ADCEDC2C208E1037ADC6 /* LaunchScreen.storyboard */, + 7D12035C470ED9DAF55A709E /* deep-link-example_iOS */, + 84EADC52DA26583ACE816A6D /* Externals */, + 146BAF1D709F8A0FE5B07709 /* Sources */, + EEC07EE3E5E2B16228B36C78 /* src */, + 308F912BBFBABA6B939FA2B3 /* Frameworks */, + F9EEBB3248B74B1D6CDA4D84 /* Products */, + ); + sourceTree = ""; + }; + 308F912BBFBABA6B939FA2B3 /* Frameworks */ = { + isa = PBXGroup; + children = ( + B18865218362A4BE07527DBD /* CoreGraphics.framework */, + 403FB4BAE59F74EE98EF1EC6 /* libapp.a */, + ED2B1BC06DFE0498ECDEEE51 /* Metal.framework */, + 1C21C8B4A18EC7D0B5808C10 /* MetalKit.framework */, + 9435FC7E183EA6260CE76637 /* QuartzCore.framework */, + 4BDECB1ED2EEEB5A6A8B8372 /* Security.framework */, + 2F316D1CD78DD2E070DA5C17 /* UIKit.framework */, + 6BDF5DBBA740DA7D86791DEC /* WebKit.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; + 7D12035C470ED9DAF55A709E /* deep-link-example_iOS */ = { + isa = PBXGroup; + children = ( + 8AB0099573FE8BF1DC82CDBA /* deep-link-example_iOS.entitlements */, + B005488D1B56B657AB52E28C /* Info.plist */, + ); + path = "deep-link-example_iOS"; + sourceTree = ""; + }; + 84EADC52DA26583ACE816A6D /* Externals */ = { + isa = PBXGroup; + children = ( + ); + path = Externals; + sourceTree = ""; + }; + 9FE22F548D05F1B0C03527E4 /* bindings */ = { + isa = PBXGroup; + children = ( + 3DD32303BEC377C10162CF69 /* bindings.h */, + ); + path = bindings; + sourceTree = ""; + }; + EEC07EE3E5E2B16228B36C78 /* src */ = { + isa = PBXGroup; + children = ( + 4A33212233BFAA738F6A46FC /* lib.rs */, + AEA78299D25FEC31E2988090 /* main.rs */, + ); + name = src; + path = ../../src; + sourceTree = ""; + }; + F9EEBB3248B74B1D6CDA4D84 /* Products */ = { + isa = PBXGroup; + children = ( + 1CAAFA750FD735A285DC1238 /* deep-link-example.app */, + ); + name = Products; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + A1C635908C823A89928264CD /* deep-link-example_iOS */ = { + isa = PBXNativeTarget; + buildConfigurationList = CC3B2B9068F702AB6ED6E36C /* Build configuration list for PBXNativeTarget "deep-link-example_iOS" */; + buildPhases = ( + E8BEC9005266B4780C27DC05 /* Build Rust Code */, + EAFF3E530DA24F7AB759ABB3 /* Sources */, + 8E775D86229F98E9F18AAAB7 /* Resources */, + 5C340AB143FB1483D2013F8B /* Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = "deep-link-example_iOS"; + packageProductDependencies = ( + ); + productName = "deep-link-example_iOS"; + productReference = 1CAAFA750FD735A285DC1238 /* deep-link-example.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + BCB4BA6E81088C5B470E3436 /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = YES; + LastUpgradeCheck = 1430; + }; + buildConfigurationList = 8FCB58B8ADB9F9CB9ECE01FA /* Build configuration list for PBXProject "deep-link-example" */; + compatibilityVersion = "Xcode 14.0"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + Base, + en, + ); + mainGroup = 1DC58B1705AA3ECC6B887FE7; + minimizedProjectReferenceProxies = 1; + projectDirPath = ""; + projectRoot = ""; + targets = ( + A1C635908C823A89928264CD /* deep-link-example_iOS */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 8E775D86229F98E9F18AAAB7 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 65A8D948440EDAA7F62BA1F4 /* Assets.xcassets in Resources */, + E6992F2651B864B15ED14925 /* LaunchScreen.storyboard in Resources */, + C384FB77F116B05F8E642CA8 /* assets in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + E8BEC9005266B4780C27DC05 /* Build Rust Code */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + name = "Build Rust Code"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(SRCROOT)/Externals/x86_64/${CONFIGURATION}/libapp.a", + "$(SRCROOT)/Externals/arm64/${CONFIGURATION}/libapp.a", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "pnpm tauri ios xcode-script -v --platform ${PLATFORM_DISPLAY_NAME:?} --sdk-root ${SDKROOT:?} --framework-search-paths \"${FRAMEWORK_SEARCH_PATHS:?}\" --header-search-paths \"${HEADER_SEARCH_PATHS:?}\" --gcc-preprocessor-definitions \"${GCC_PREPROCESSOR_DEFINITIONS:-}\" --configuration ${CONFIGURATION:?} ${FORCE_COLOR} ${ARCHS:?}"; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + EAFF3E530DA24F7AB759ABB3 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + D4D232DBB85C5C1594FACC3D /* main.mm in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin XCBuildConfiguration section */ + 21DF092E6F2020CEC95B687A /* debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "$(inherited)", + "DEBUG=1", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = iphoneos; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + }; + name = debug; + }; + 3655D9D0A68600F8988F24EB /* debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + ARCHS = arm64; + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CODE_SIGN_ENTITLEMENTS = "deep-link-example_iOS/deep-link-example_iOS.entitlements"; + CODE_SIGN_IDENTITY = "iPhone Developer"; + DEVELOPMENT_TEAM = "Q93MBH6S2F"; + ENABLE_BITCODE = NO; + "EXCLUDED_ARCHS[sdk=iphoneos*]" = x86_64; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "\".\"", + ); + INFOPLIST_FILE = "deep-link-example_iOS/Info.plist"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + "LIBRARY_SEARCH_PATHS[arch=arm64]" = ( + "$(inherited)", + "$(PROJECT_DIR)/Externals/arm64/$(CONFIGURATION)", + "$(SDKROOT)/usr/lib/swift", + "$(TOOLCHAIN_DIR)/usr/lib/swift/$(PLATFORM_NAME)", + "$(TOOLCHAIN_DIR)/usr/lib/swift-5.0/$(PLATFORM_NAME)", + ); + "LIBRARY_SEARCH_PATHS[arch=x86_64]" = ( + "$(inherited)", + "$(PROJECT_DIR)/Externals/x86_64/$(CONFIGURATION)", + "$(SDKROOT)/usr/lib/swift", + "$(TOOLCHAIN_DIR)/usr/lib/swift/$(PLATFORM_NAME)", + "$(TOOLCHAIN_DIR)/usr/lib/swift-5.0/$(PLATFORM_NAME)", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.tauri.deep-link-example; + PRODUCT_NAME = "deep-link-example"; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + VALID_ARCHS = arm64; + }; + name = debug; + }; + 7A19D7CB4CA2808477E73B8A /* release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + SWIFT_VERSION = 5.0; + }; + name = release; + }; + 7C085E1672AF39D75FFD0081 /* release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + ARCHS = arm64; + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CODE_SIGN_ENTITLEMENTS = "deep-link-example_iOS/deep-link-example_iOS.entitlements"; + CODE_SIGN_IDENTITY = "iPhone Developer"; + DEVELOPMENT_TEAM = "Q93MBH6S2F"; + ENABLE_BITCODE = NO; + "EXCLUDED_ARCHS[sdk=iphoneos*]" = x86_64; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "\".\"", + ); + INFOPLIST_FILE = "deep-link-example_iOS/Info.plist"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + "LIBRARY_SEARCH_PATHS[arch=arm64]" = ( + "$(inherited)", + "$(PROJECT_DIR)/Externals/arm64/$(CONFIGURATION)", + "$(SDKROOT)/usr/lib/swift", + "$(TOOLCHAIN_DIR)/usr/lib/swift/$(PLATFORM_NAME)", + "$(TOOLCHAIN_DIR)/usr/lib/swift-5.0/$(PLATFORM_NAME)", + ); + "LIBRARY_SEARCH_PATHS[arch=x86_64]" = ( + "$(inherited)", + "$(PROJECT_DIR)/Externals/x86_64/$(CONFIGURATION)", + "$(SDKROOT)/usr/lib/swift", + "$(TOOLCHAIN_DIR)/usr/lib/swift/$(PLATFORM_NAME)", + "$(TOOLCHAIN_DIR)/usr/lib/swift-5.0/$(PLATFORM_NAME)", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.tauri.deep-link-example; + PRODUCT_NAME = "deep-link-example"; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + VALID_ARCHS = arm64; + }; + name = release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 8FCB58B8ADB9F9CB9ECE01FA /* Build configuration list for PBXProject "deep-link-example" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 21DF092E6F2020CEC95B687A /* debug */, + 7A19D7CB4CA2808477E73B8A /* release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = debug; + }; + CC3B2B9068F702AB6ED6E36C /* Build configuration list for PBXNativeTarget "deep-link-example_iOS" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 3655D9D0A68600F8988F24EB /* debug */, + 7C085E1672AF39D75FFD0081 /* release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = debug; + }; +/* End XCConfigurationList section */ + }; + rootObject = BCB4BA6E81088C5B470E3436 /* Project object */; +} diff --git a/plugins/deep-link/examples/app/src-tauri/gen/apple/deep-link-example.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/plugins/deep-link/examples/app/src-tauri/gen/apple/deep-link-example.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..919434a --- /dev/null +++ b/plugins/deep-link/examples/app/src-tauri/gen/apple/deep-link-example.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/plugins/deep-link/examples/app/src-tauri/gen/apple/deep-link-example.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/plugins/deep-link/examples/app/src-tauri/gen/apple/deep-link-example.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 0000000..ac90d5a --- /dev/null +++ b/plugins/deep-link/examples/app/src-tauri/gen/apple/deep-link-example.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,10 @@ + + + + + BuildSystemType + Original + DisableBuildSystemDeprecationDiagnostic + + + diff --git a/plugins/deep-link/examples/app/src-tauri/gen/apple/deep-link-example.xcodeproj/xcshareddata/xcschemes/deep-link-example_iOS.xcscheme b/plugins/deep-link/examples/app/src-tauri/gen/apple/deep-link-example.xcodeproj/xcshareddata/xcschemes/deep-link-example_iOS.xcscheme new file mode 100644 index 0000000..a8fbc97 --- /dev/null +++ b/plugins/deep-link/examples/app/src-tauri/gen/apple/deep-link-example.xcodeproj/xcshareddata/xcschemes/deep-link-example_iOS.xcscheme @@ -0,0 +1,123 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/plugins/deep-link/examples/app/src-tauri/gen/apple/deep-link-example_iOS/Info.plist b/plugins/deep-link/examples/app/src-tauri/gen/apple/deep-link-example_iOS/Info.plist new file mode 100644 index 0000000..bc74b01 --- /dev/null +++ b/plugins/deep-link/examples/app/src-tauri/gen/apple/deep-link-example_iOS/Info.plist @@ -0,0 +1,55 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + 0.1.0 + CFBundleVersion + 0.1.0 + LSRequiresIPhoneOS + + UILaunchStoryboardName + LaunchScreen + UIRequiredDeviceCapabilities + + arm64 + metal + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + CFBundleURLTypes + + + CFBundleURLSchemes + + taurideeplink + + CFBundleURLName + taurideeplink + + + + \ No newline at end of file diff --git a/plugins/deep-link/examples/app/src-tauri/gen/apple/deep-link-example_iOS/deep-link-example_iOS.entitlements b/plugins/deep-link/examples/app/src-tauri/gen/apple/deep-link-example_iOS/deep-link-example_iOS.entitlements new file mode 100644 index 0000000..3216c74 --- /dev/null +++ b/plugins/deep-link/examples/app/src-tauri/gen/apple/deep-link-example_iOS/deep-link-example_iOS.entitlements @@ -0,0 +1,11 @@ + + + + + com.apple.developer.associated-domains + + applinks:fabianlars.de + applinks:tauri.app + + + \ No newline at end of file diff --git a/plugins/deep-link/examples/app/src-tauri/gen/apple/project.yml b/plugins/deep-link/examples/app/src-tauri/gen/apple/project.yml new file mode 100644 index 0000000..74d0ab4 --- /dev/null +++ b/plugins/deep-link/examples/app/src-tauri/gen/apple/project.yml @@ -0,0 +1,88 @@ +name: deep-link-example +options: + bundleIdPrefix: com.tauri.deep-link-example + deploymentTarget: + iOS: 13.0 +fileGroups: [../../src] +configs: + debug: debug + release: release +settingGroups: + app: + base: + PRODUCT_NAME: deep-link-example + PRODUCT_BUNDLE_IDENTIFIER: com.tauri.deep-link-example +targetTemplates: + app: + type: application + sources: + - path: Sources + scheme: + environmentVariables: + RUST_BACKTRACE: full + RUST_LOG: info + settings: + groups: [app] +targets: + deep-link-example_iOS: + type: application + platform: iOS + sources: + - path: Sources + - path: Assets.xcassets + - path: Externals + - path: deep-link-example_iOS + - path: assets + buildPhase: resources + type: folder + - path: LaunchScreen.storyboard + info: + path: deep-link-example_iOS/Info.plist + properties: + LSRequiresIPhoneOS: true + UILaunchStoryboardName: LaunchScreen + UIRequiredDeviceCapabilities: [arm64, metal] + UISupportedInterfaceOrientations: + - UIInterfaceOrientationPortrait + - UIInterfaceOrientationLandscapeLeft + - UIInterfaceOrientationLandscapeRight + UISupportedInterfaceOrientations~ipad: + - UIInterfaceOrientationPortrait + - UIInterfaceOrientationPortraitUpsideDown + - UIInterfaceOrientationLandscapeLeft + - UIInterfaceOrientationLandscapeRight + CFBundleShortVersionString: 0.1.0 + CFBundleVersion: '0.1.0' + entitlements: + path: deep-link-example_iOS/deep-link-example_iOS.entitlements + scheme: + environmentVariables: + RUST_BACKTRACE: full + RUST_LOG: info + settings: + base: + ENABLE_BITCODE: false + ARCHS: [arm64] + VALID_ARCHS: arm64 + LIBRARY_SEARCH_PATHS[arch=x86_64]: $(inherited) $(PROJECT_DIR)/Externals/x86_64/$(CONFIGURATION) $(SDKROOT)/usr/lib/swift $(TOOLCHAIN_DIR)/usr/lib/swift/$(PLATFORM_NAME) $(TOOLCHAIN_DIR)/usr/lib/swift-5.0/$(PLATFORM_NAME) + LIBRARY_SEARCH_PATHS[arch=arm64]: $(inherited) $(PROJECT_DIR)/Externals/arm64/$(CONFIGURATION) $(SDKROOT)/usr/lib/swift $(TOOLCHAIN_DIR)/usr/lib/swift/$(PLATFORM_NAME) $(TOOLCHAIN_DIR)/usr/lib/swift-5.0/$(PLATFORM_NAME) + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES: true + EXCLUDED_ARCHS[sdk=iphoneos*]: x86_64 + groups: [app] + dependencies: + - framework: libapp.a + embed: false + - sdk: CoreGraphics.framework + - sdk: Metal.framework + - sdk: MetalKit.framework + - sdk: QuartzCore.framework + - sdk: Security.framework + - sdk: UIKit.framework + - sdk: WebKit.framework + preBuildScripts: + - script: pnpm tauri ios xcode-script -v --platform ${PLATFORM_DISPLAY_NAME:?} --sdk-root ${SDKROOT:?} --framework-search-paths "${FRAMEWORK_SEARCH_PATHS:?}" --header-search-paths "${HEADER_SEARCH_PATHS:?}" --gcc-preprocessor-definitions "${GCC_PREPROCESSOR_DEFINITIONS:-}" --configuration ${CONFIGURATION:?} ${FORCE_COLOR} ${ARCHS:?} + name: Build Rust Code + basedOnDependencyAnalysis: false + outputFiles: + - $(SRCROOT)/Externals/x86_64/${CONFIGURATION}/libapp.a + - $(SRCROOT)/Externals/arm64/${CONFIGURATION}/libapp.a diff --git a/plugins/deep-link/examples/app/src-tauri/icons/128x128.png b/plugins/deep-link/examples/app/src-tauri/icons/128x128.png new file mode 100644 index 0000000000000000000000000000000000000000..77e7d2338e9d8ccffc731198dc584906627c903f GIT binary patch literal 11059 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

j#(n6`cTK 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 String { + format!("Hello, {name}! You've been greeted from Rust!") +} + +#[cfg_attr(mobile, tauri::mobile_entry_point)] +pub fn run() { + #[allow(unused_mut)] + let mut builder = tauri::Builder::default(); + + #[cfg(desktop)] + { + builder = builder.plugin(tauri_plugin_single_instance::init(|_app, argv, _cwd| { + println!("single instance triggered: {argv:?}"); + })); + } + + builder + .plugin(tauri_plugin_deep_link::init()) + .plugin( + tauri_plugin_log::Builder::default() + .level(log::LevelFilter::Info) + .build(), + ) + .setup(|app| { + // ensure deep links are registered on the system + // this is useful because AppImages requires additional setup to be available in the system + // and calling register() makes the deep links immediately available - without any user input + // additionally, we manually register on Windows on debug builds for development + #[cfg(any(target_os = "linux", all(debug_assertions, windows)))] + app.deep_link().register_all()?; + + app.deep_link().on_open_url(|event| { + dbg!(event.urls()); + }); + + Ok(()) + }) + .invoke_handler(tauri::generate_handler![greet]) + .run(tauri::generate_context!()) + .expect("error while running tauri application"); +} diff --git a/plugins/deep-link/examples/app/src-tauri/src/main.rs b/plugins/deep-link/examples/app/src-tauri/src/main.rs new file mode 100644 index 0000000..799fad7 --- /dev/null +++ b/plugins/deep-link/examples/app/src-tauri/src/main.rs @@ -0,0 +1,11 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +// Prevents additional console window on Windows in release, DO NOT REMOVE!! +#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] + +fn main() { + #[cfg(desktop)] + deep_link_example::run(); +} diff --git a/plugins/deep-link/examples/app/src-tauri/tauri.conf.json b/plugins/deep-link/examples/app/src-tauri/tauri.conf.json new file mode 100644 index 0000000..61f7394 --- /dev/null +++ b/plugins/deep-link/examples/app/src-tauri/tauri.conf.json @@ -0,0 +1,59 @@ +{ + "productName": "deep-link-example", + "version": "0.1.0", + "identifier": "com.tauri.deep-link-example", + "build": { + "devUrl": "http://localhost:1420", + "frontendDist": "../dist", + "beforeDevCommand": "pnpm dev", + "beforeBuildCommand": "pnpm build" + }, + "app": { + "security": { + "csp": null + }, + "windows": [ + { + "fullscreen": false, + "height": 600, + "resizable": true, + "title": "tauri-app", + "width": 800 + } + ] + }, + "plugins": { + "whatever": "helloworld", + "sec": { + "hello": "world" + }, + "deep-link": { + "mobile": [ + { + "host": "fabianlars.de", + "pathPrefix": ["/intent"] + }, + { + "host": "tauri.app" + }, + { + "scheme": ["taurideeplink"] + } + ], + "desktop": { + "schemes": ["fabianlars", "my-tauri-app"] + } + } + }, + "bundle": { + "active": true, + "category": "DeveloperTool", + "icon": [ + "icons/32x32.png", + "icons/128x128.png", + "icons/128x128@2x.png", + "icons/icon.icns", + "icons/icon.ico" + ] + } +} diff --git a/plugins/deep-link/examples/app/src/assets/tauri.svg b/plugins/deep-link/examples/app/src/assets/tauri.svg new file mode 100644 index 0000000..31b62c9 --- /dev/null +++ b/plugins/deep-link/examples/app/src/assets/tauri.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/plugins/deep-link/examples/app/src/assets/typescript.svg b/plugins/deep-link/examples/app/src/assets/typescript.svg new file mode 100644 index 0000000..30a5edd --- /dev/null +++ b/plugins/deep-link/examples/app/src/assets/typescript.svg @@ -0,0 +1,25 @@ + + + + + + + + diff --git a/plugins/deep-link/examples/app/src/assets/vite.svg b/plugins/deep-link/examples/app/src/assets/vite.svg new file mode 100644 index 0000000..e7b8dfb --- /dev/null +++ b/plugins/deep-link/examples/app/src/assets/vite.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/plugins/deep-link/examples/app/src/main.ts b/plugins/deep-link/examples/app/src/main.ts new file mode 100644 index 0000000..550e0aa --- /dev/null +++ b/plugins/deep-link/examples/app/src/main.ts @@ -0,0 +1,38 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +import { + onOpenUrl, + getCurrent as getCurrentDeepLinkUrls +} from '@tauri-apps/plugin-deep-link' + +function handler(urls: string[]) { + console.log(urls) + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const updateIntentEl = document.querySelector('#event-intent')! + updateIntentEl.textContent = JSON.stringify(urls) +} + +window.addEventListener('DOMContentLoaded', () => { + onOpenUrl(handler) + + document.querySelector('#intent-form')?.addEventListener('submit', (e) => { + e.preventDefault() + getCurrentDeepLinkUrls() + .then((res) => { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const updateIntentEl = document.querySelector('#update-intent')! + updateIntentEl.textContent = res ? JSON.stringify(res) : 'none' + }) + .catch(console.error) + }) + + getCurrentDeepLinkUrls() + .then((res) => { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const initialIntentEl = document.querySelector('#initial-intent')! + initialIntentEl.textContent = res ? JSON.stringify(res) : 'none' + }) + .catch(console.error) +}) diff --git a/plugins/deep-link/examples/app/src/styles.css b/plugins/deep-link/examples/app/src/styles.css new file mode 100644 index 0000000..f7de85b --- /dev/null +++ b/plugins/deep-link/examples/app/src/styles.css @@ -0,0 +1,109 @@ +:root { + font-family: Inter, Avenir, Helvetica, Arial, sans-serif; + font-size: 16px; + line-height: 24px; + font-weight: 400; + + color: #0f0f0f; + background-color: #f6f6f6; + + font-synthesis: none; + text-rendering: optimizeLegibility; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + -webkit-text-size-adjust: 100%; +} + +.container { + margin: 0; + padding-top: 10vh; + display: flex; + flex-direction: column; + justify-content: center; + text-align: center; +} + +.logo { + height: 6em; + padding: 1.5em; + will-change: filter; + transition: 0.75s; +} + +.logo.tauri:hover { + filter: drop-shadow(0 0 2em #24c8db); +} + +.row { + display: flex; + justify-content: center; +} + +a { + font-weight: 500; + color: #646cff; + text-decoration: inherit; +} + +a:hover { + color: #535bf2; +} + +h1 { + text-align: center; +} + +input, +button { + border-radius: 8px; + border: 1px solid transparent; + padding: 0.6em 1.2em; + font-size: 1em; + font-weight: 500; + font-family: inherit; + color: #0f0f0f; + background-color: #ffffff; + transition: border-color 0.25s; + box-shadow: 0 2px 2px rgba(0, 0, 0, 0.2); +} + +button { + cursor: pointer; +} + +button:hover { + border-color: #396cd8; +} +button:active { + border-color: #396cd8; + background-color: #e8e8e8; +} + +input, +button { + outline: none; +} + +#greet-input { + margin-right: 5px; +} + +@media (prefers-color-scheme: dark) { + :root { + color: #f6f6f6; + background-color: #2f2f2f; + } + + a:hover { + color: #24c8db; + } + + input, + button { + color: #ffffff; + background-color: #0f0f0f98; + } + button:active { + background-color: #0f0f0f69; + } +} diff --git a/plugins/deep-link/examples/app/tsconfig.json b/plugins/deep-link/examples/app/tsconfig.json new file mode 100644 index 0000000..c0f337a --- /dev/null +++ b/plugins/deep-link/examples/app/tsconfig.json @@ -0,0 +1,18 @@ +{ + "compilerOptions": { + "target": "ESNext", + "module": "ESNext", + "lib": ["ESNext", "DOM"], + "moduleResolution": "bundler", + "strict": true, + "sourceMap": true, + "resolveJsonModule": true, + "esModuleInterop": true, + "types": ["vite/client"], + "noEmit": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noImplicitReturns": true + }, + "include": ["./src"] +} diff --git a/plugins/deep-link/examples/app/vite.config.ts b/plugins/deep-link/examples/app/vite.config.ts new file mode 100644 index 0000000..b54dc99 --- /dev/null +++ b/plugins/deep-link/examples/app/vite.config.ts @@ -0,0 +1,38 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +import { defineConfig } from 'vite' + +const host = process.env.TAURI_DEV_HOST + +// https://vitejs.dev/config/ +export default defineConfig({ + // Vite options tailored for Tauri development and only applied in `tauri dev` or `tauri build` + // prevent vite from obscuring rust errors + clearScreen: false, + // tauri expects a fixed port, fail if that port is not available + server: { + host: host || false, + port: 1420, + hmr: host + ? { + protocol: 'ws', + host, + port: 1421 + } + : undefined, + strictPort: true + }, + // to make use of `TAURI_DEBUG` and other env variables + // https://tauri.studio/v1/api/config#buildconfig.beforedevcommand + envPrefix: ['VITE_', 'TAURI_'], + build: { + // Tauri supports es2021 + target: process.env.TAURI_PLATFORM == 'windows' ? 'chrome105' : 'safari13', + // don't minify for debug builds + minify: !process.env.TAURI_DEBUG ? 'esbuild' : false, + // produce sourcemaps for debug builds + sourcemap: !!process.env.TAURI_DEBUG + } +}) diff --git a/plugins/deep-link/guest-js/index.ts b/plugins/deep-link/guest-js/index.ts new file mode 100644 index 0000000..41b2fe8 --- /dev/null +++ b/plugins/deep-link/guest-js/index.ts @@ -0,0 +1,128 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +/** + * Set your Tauri application as the default handler for a URL, or check which URL(s) it was opened with. + * + * @module + */ + +import { invoke } from '@tauri-apps/api/core' +import { type UnlistenFn, listen } from '@tauri-apps/api/event' + +/** + * Get the current URLs that triggered the deep link. Use this on app load to check whether your app was started via a deep link. + * + * @example + * ```typescript + * import { getCurrent } from '@tauri-apps/plugin-deep-link'; + * const urls = await getCurrent(); + * ``` + * + * #### Platform-specific + * + * - **Windows / Linux:** This function reads the command line arguments and checks if there's only one value, which must be an URL with scheme matching one of the configured values. + * Note that you must manually check the arguments when registering deep link schemes dynamically with {@link register}. + * Additionally, the deep link might have been provided as a CLI argument so you should check if its format matches what you expect. + * + * @returns A promise resolving to the list of URLs that triggered the deep link, or `null` if the app was not started via a deep link. + * @since 2.0.0 + */ +export async function getCurrent(): Promise { + return await invoke('plugin:deep-link|get_current') +} + +/** + * Register the app as the default handler for the specified protocol. + * + * @param protocol The name of the protocol without `://`. For example, if you want your app to handle `tauri://` links, call this method with `tauri` as the protocol. + * + * @example + * ```typescript + * import { register } from '@tauri-apps/plugin-deep-link'; + * await register("my-scheme"); + * ``` + * + * #### Platform-specific + * + * - **macOS / Android / iOS:** Unsupported. + * + * @returns A promise that resolves once the protocol has been registered. + * @since 2.0.0 + */ +export async function register(protocol: string): Promise { + return await invoke('plugin:deep-link|register', { protocol }) +} + +/** + * Unregister the app as the default handler for the specified protocol. + * + * @param protocol The name of the protocol without `://`. + * + * @example + * ```typescript + * import { unregister } from '@tauri-apps/plugin-deep-link'; + * await unregister("my-scheme"); + * ``` + * + * #### Platform-specific + * + * - **Windows:** Requires admin rights if the protocol is registered on the local machine (this can happen when registered from the NSIS installer when the install mode is set to both or per machine). + * - **Linux:** Can only unregister the scheme if it was initially registered with {@link register}. May not work on older distros. + * - **macOS / Android / iOS:** Unsupported. + * + * @returns A promise that resolves once the protocol has been unregistered. + * @since 2.0.0 + */ +export async function unregister(protocol: string): Promise { + return await invoke('plugin:deep-link|unregister', { protocol }) +} + +/** + * Check whether the app is the default handler for the specified protocol. + * + * @param protocol The name of the protocol without `://`. + * + * @example + * ```typescript + * import { isRegistered } from '@tauri-apps/plugin-deep-link'; + * await isRegistered("my-scheme"); + * ``` + * + * #### Platform-specific + * + * - **macOS / Android / iOS:** Unsupported. + * + * @returns A promise resolving to `true` if the app is the default handler for the protocol, `false` otherwise. + * @since 2.0.0 + */ +export async function isRegistered(protocol: string): Promise { + return await invoke('plugin:deep-link|is_registered', { protocol }) +} + +/** + * Helper function for the `deep-link://new-url` event to run a function each time the protocol is triggered while the app is running. Use {@link getCurrent} on app load to check whether your app was started via a deep link. + * + * @param handler The function to call with the list of URLs the app was requested to open, every time this happens while the app is running. + * + * @example + * ```typescript + * import { onOpenUrl } from '@tauri-apps/plugin-deep-link'; + * await onOpenUrl((urls) => { console.log(urls) }); + * ``` + * + * #### Platform-specific + * + * - **Windows / Linux:** Unsupported without the single-instance plugin. The OS will spawn a new app instance passing the URL as a CLI argument. + * + * @returns A promise resolving to a function that unregisters the event listener. + * @since 2.0.0 + */ +export async function onOpenUrl( + handler: (urls: string[]) => void +): Promise { + return await listen('deep-link://new-url', (event) => { + handler(event.payload) + }) +} diff --git a/plugins/deep-link/package.json b/plugins/deep-link/package.json new file mode 100644 index 0000000..3d21b7e --- /dev/null +++ b/plugins/deep-link/package.json @@ -0,0 +1,33 @@ +{ + "name": "@tauri-apps/plugin-deep-link", + "version": "2.4.10", + "description": "Set your Tauri application as the default handler for an URL", + "license": "MIT OR Apache-2.0", + "authors": [ + "Tauri Programme within The Commons Conservancy" + ], + "repository": "https://github.com/tauri-apps/plugins-workspace", + "type": "module", + "types": "./dist-js/index.d.ts", + "main": "./dist-js/index.cjs", + "module": "./dist-js/index.js", + "exports": { + "types": "./dist-js/index.d.ts", + "import": "./dist-js/index.js", + "require": "./dist-js/index.cjs" + }, + "scripts": { + "build": "rollup -c" + }, + "files": [ + "dist-js", + "README.md", + "LICENSE" + ], + "dependencies": { + "@tauri-apps/api": "^2.11.0" + }, + "devDependencies": { + "@tauri-apps/cli": "2.11.4" + } +} diff --git a/plugins/deep-link/permissions/autogenerated/commands/get_current.toml b/plugins/deep-link/permissions/autogenerated/commands/get_current.toml new file mode 100644 index 0000000..e729de1 --- /dev/null +++ b/plugins/deep-link/permissions/autogenerated/commands/get_current.toml @@ -0,0 +1,13 @@ +# Automatically generated - DO NOT EDIT! + +"$schema" = "../../schemas/schema.json" + +[[permission]] +identifier = "allow-get-current" +description = "Enables the get_current command without any pre-configured scope." +commands.allow = ["get_current"] + +[[permission]] +identifier = "deny-get-current" +description = "Denies the get_current command without any pre-configured scope." +commands.deny = ["get_current"] diff --git a/plugins/deep-link/permissions/autogenerated/commands/is_registered.toml b/plugins/deep-link/permissions/autogenerated/commands/is_registered.toml new file mode 100644 index 0000000..2dd73ac --- /dev/null +++ b/plugins/deep-link/permissions/autogenerated/commands/is_registered.toml @@ -0,0 +1,13 @@ +# Automatically generated - DO NOT EDIT! + +"$schema" = "../../schemas/schema.json" + +[[permission]] +identifier = "allow-is-registered" +description = "Enables the is_registered command without any pre-configured scope." +commands.allow = ["is_registered"] + +[[permission]] +identifier = "deny-is-registered" +description = "Denies the is_registered command without any pre-configured scope." +commands.deny = ["is_registered"] diff --git a/plugins/deep-link/permissions/autogenerated/commands/register.toml b/plugins/deep-link/permissions/autogenerated/commands/register.toml new file mode 100644 index 0000000..4eec17d --- /dev/null +++ b/plugins/deep-link/permissions/autogenerated/commands/register.toml @@ -0,0 +1,13 @@ +# Automatically generated - DO NOT EDIT! + +"$schema" = "../../schemas/schema.json" + +[[permission]] +identifier = "allow-register" +description = "Enables the register command without any pre-configured scope." +commands.allow = ["register"] + +[[permission]] +identifier = "deny-register" +description = "Denies the register command without any pre-configured scope." +commands.deny = ["register"] diff --git a/plugins/deep-link/permissions/autogenerated/commands/unregister.toml b/plugins/deep-link/permissions/autogenerated/commands/unregister.toml new file mode 100644 index 0000000..5d33c97 --- /dev/null +++ b/plugins/deep-link/permissions/autogenerated/commands/unregister.toml @@ -0,0 +1,13 @@ +# Automatically generated - DO NOT EDIT! + +"$schema" = "../../schemas/schema.json" + +[[permission]] +identifier = "allow-unregister" +description = "Enables the unregister command without any pre-configured scope." +commands.allow = ["unregister"] + +[[permission]] +identifier = "deny-unregister" +description = "Denies the unregister command without any pre-configured scope." +commands.deny = ["unregister"] diff --git a/plugins/deep-link/permissions/autogenerated/reference.md b/plugins/deep-link/permissions/autogenerated/reference.md new file mode 100644 index 0000000..a8ef187 --- /dev/null +++ b/plugins/deep-link/permissions/autogenerated/reference.md @@ -0,0 +1,121 @@ +## Default Permission + +Allows reading the opened deep link via the get_current command + +#### This default permission set includes the following: + +- `allow-get-current` + +## Permission Table + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
IdentifierDescription
+ +`deep-link:allow-get-current` + + + +Enables the get_current command without any pre-configured scope. + +
+ +`deep-link:deny-get-current` + + + +Denies the get_current command without any pre-configured scope. + +
+ +`deep-link:allow-is-registered` + + + +Enables the is_registered command without any pre-configured scope. + +
+ +`deep-link:deny-is-registered` + + + +Denies the is_registered command without any pre-configured scope. + +
+ +`deep-link:allow-register` + + + +Enables the register command without any pre-configured scope. + +
+ +`deep-link:deny-register` + + + +Denies the register command without any pre-configured scope. + +
+ +`deep-link:allow-unregister` + + + +Enables the unregister command without any pre-configured scope. + +
+ +`deep-link:deny-unregister` + + + +Denies the unregister command without any pre-configured scope. + +
diff --git a/plugins/deep-link/permissions/default.toml b/plugins/deep-link/permissions/default.toml new file mode 100644 index 0000000..0c3cf2c --- /dev/null +++ b/plugins/deep-link/permissions/default.toml @@ -0,0 +1,4 @@ +"$schema" = "schemas/schema.json" +[default] +description = "Allows reading the opened deep link via the get_current command" +permissions = ["allow-get-current"] diff --git a/plugins/deep-link/permissions/schemas/schema.json b/plugins/deep-link/permissions/schemas/schema.json new file mode 100644 index 0000000..c9fc2ce --- /dev/null +++ b/plugins/deep-link/permissions/schemas/schema.json @@ -0,0 +1,354 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "PermissionFile", + "description": "Permission file that can define a default permission, a set of permissions or a list of inlined permissions.", + "type": "object", + "properties": { + "default": { + "description": "The default permission set for the plugin", + "anyOf": [ + { + "$ref": "#/definitions/DefaultPermission" + }, + { + "type": "null" + } + ] + }, + "set": { + "description": "A list of permissions sets defined", + "type": "array", + "items": { + "$ref": "#/definitions/PermissionSet" + } + }, + "permission": { + "description": "A list of inlined permissions", + "default": [], + "type": "array", + "items": { + "$ref": "#/definitions/Permission" + } + } + }, + "definitions": { + "DefaultPermission": { + "description": "The default permission set of the plugin.\n\nWorks similarly to a permission with the \"default\" identifier.", + "type": "object", + "required": [ + "permissions" + ], + "properties": { + "version": { + "description": "The version of the permission.", + "type": [ + "integer", + "null" + ], + "format": "uint64", + "minimum": 1.0 + }, + "description": { + "description": "Human-readable description of what the permission does. Tauri convention is to use `

` headings in markdown content for Tauri documentation generation purposes.", + "type": [ + "string", + "null" + ] + }, + "permissions": { + "description": "All permissions this set contains.", + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "PermissionSet": { + "description": "A set of direct permissions grouped together under a new name.", + "type": "object", + "required": [ + "description", + "identifier", + "permissions" + ], + "properties": { + "identifier": { + "description": "A unique identifier for the permission.", + "type": "string" + }, + "description": { + "description": "Human-readable description of what the permission does.", + "type": "string" + }, + "permissions": { + "description": "All permissions this set contains.", + "type": "array", + "items": { + "$ref": "#/definitions/PermissionKind" + } + } + } + }, + "Permission": { + "description": "Descriptions of explicit privileges of commands.\n\nIt can enable commands to be accessible in the frontend of the application.\n\nIf the scope is defined it can be used to fine grain control the access of individual or multiple commands.", + "type": "object", + "required": [ + "identifier" + ], + "properties": { + "version": { + "description": "The version of the permission.", + "type": [ + "integer", + "null" + ], + "format": "uint64", + "minimum": 1.0 + }, + "identifier": { + "description": "A unique identifier for the permission.", + "type": "string" + }, + "description": { + "description": "Human-readable description of what the permission does. Tauri internal convention is to use `

` headings in markdown content for Tauri documentation generation purposes.", + "type": [ + "string", + "null" + ] + }, + "commands": { + "description": "Allowed or denied commands when using this permission.", + "default": { + "allow": [], + "deny": [] + }, + "allOf": [ + { + "$ref": "#/definitions/Commands" + } + ] + }, + "scope": { + "description": "Allowed or denied scoped when using this permission.", + "allOf": [ + { + "$ref": "#/definitions/Scopes" + } + ] + }, + "platforms": { + "description": "Target platforms this permission applies. By default all platforms are affected by this permission.", + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/Target" + } + } + } + }, + "Commands": { + "description": "Allowed and denied commands inside a permission.\n\nIf two commands clash inside of `allow` and `deny`, it should be denied by default.", + "type": "object", + "properties": { + "allow": { + "description": "Allowed command.", + "default": [], + "type": "array", + "items": { + "type": "string" + } + }, + "deny": { + "description": "Denied command, which takes priority.", + "default": [], + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "Scopes": { + "description": "An argument for fine grained behavior control of Tauri commands.\n\nIt can be of any serde serializable type and is used to allow or prevent certain actions inside a Tauri command. The configured scope is passed to the command and will be enforced by the command implementation.\n\n## Example\n\n```json { \"allow\": [{ \"path\": \"$HOME/**\" }], \"deny\": [{ \"path\": \"$HOME/secret.txt\" }] } ```", + "type": "object", + "properties": { + "allow": { + "description": "Data that defines what is allowed by the scope.", + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/Value" + } + }, + "deny": { + "description": "Data that defines what is denied by the scope. This should be prioritized by validation logic.", + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/Value" + } + } + } + }, + "Value": { + "description": "All supported ACL values.", + "anyOf": [ + { + "description": "Represents a null JSON value.", + "type": "null" + }, + { + "description": "Represents a [`bool`].", + "type": "boolean" + }, + { + "description": "Represents a valid ACL [`Number`].", + "allOf": [ + { + "$ref": "#/definitions/Number" + } + ] + }, + { + "description": "Represents a [`String`].", + "type": "string" + }, + { + "description": "Represents a list of other [`Value`]s.", + "type": "array", + "items": { + "$ref": "#/definitions/Value" + } + }, + { + "description": "Represents a map of [`String`] keys to [`Value`]s.", + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/Value" + } + } + ] + }, + "Number": { + "description": "A valid ACL number.", + "anyOf": [ + { + "description": "Represents an [`i64`].", + "type": "integer", + "format": "int64" + }, + { + "description": "Represents a [`f64`].", + "type": "number", + "format": "double" + } + ] + }, + "Target": { + "description": "Platform target.", + "oneOf": [ + { + "description": "MacOS.", + "type": "string", + "enum": [ + "macOS" + ] + }, + { + "description": "Windows.", + "type": "string", + "enum": [ + "windows" + ] + }, + { + "description": "Linux.", + "type": "string", + "enum": [ + "linux" + ] + }, + { + "description": "Android.", + "type": "string", + "enum": [ + "android" + ] + }, + { + "description": "iOS.", + "type": "string", + "enum": [ + "iOS" + ] + } + ] + }, + "PermissionKind": { + "type": "string", + "oneOf": [ + { + "description": "Enables the get_current command without any pre-configured scope.", + "type": "string", + "const": "allow-get-current", + "markdownDescription": "Enables the get_current command without any pre-configured scope." + }, + { + "description": "Denies the get_current command without any pre-configured scope.", + "type": "string", + "const": "deny-get-current", + "markdownDescription": "Denies the get_current command without any pre-configured scope." + }, + { + "description": "Enables the is_registered command without any pre-configured scope.", + "type": "string", + "const": "allow-is-registered", + "markdownDescription": "Enables the is_registered command without any pre-configured scope." + }, + { + "description": "Denies the is_registered command without any pre-configured scope.", + "type": "string", + "const": "deny-is-registered", + "markdownDescription": "Denies the is_registered command without any pre-configured scope." + }, + { + "description": "Enables the register command without any pre-configured scope.", + "type": "string", + "const": "allow-register", + "markdownDescription": "Enables the register command without any pre-configured scope." + }, + { + "description": "Denies the register command without any pre-configured scope.", + "type": "string", + "const": "deny-register", + "markdownDescription": "Denies the register command without any pre-configured scope." + }, + { + "description": "Enables the unregister command without any pre-configured scope.", + "type": "string", + "const": "allow-unregister", + "markdownDescription": "Enables the unregister command without any pre-configured scope." + }, + { + "description": "Denies the unregister command without any pre-configured scope.", + "type": "string", + "const": "deny-unregister", + "markdownDescription": "Denies the unregister command without any pre-configured scope." + }, + { + "description": "Allows reading the opened deep link via the get_current command\n#### This default permission set includes:\n\n- `allow-get-current`", + "type": "string", + "const": "default", + "markdownDescription": "Allows reading the opened deep link via the get_current command\n#### This default permission set includes:\n\n- `allow-get-current`" + } + ] + } + } +} \ No newline at end of file diff --git a/plugins/deep-link/rollup.config.js b/plugins/deep-link/rollup.config.js new file mode 100644 index 0000000..1f349ec --- /dev/null +++ b/plugins/deep-link/rollup.config.js @@ -0,0 +1,7 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +import { createConfig } from '../../shared/rollup.config.js' + +export default createConfig() diff --git a/plugins/deep-link/src/commands.rs b/plugins/deep-link/src/commands.rs new file mode 100644 index 0000000..078adfb --- /dev/null +++ b/plugins/deep-link/src/commands.rs @@ -0,0 +1,46 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use tauri::{command, AppHandle, Runtime, State, Window}; + +use crate::{DeepLink, Result}; + +#[command] +pub(crate) async fn get_current( + _app: AppHandle, + _window: Window, + deep_link: State<'_, DeepLink>, +) -> Result>> { + deep_link.get_current() +} + +#[command] +pub(crate) async fn register( + _app: AppHandle, + _window: Window, + deep_link: State<'_, DeepLink>, + protocol: String, +) -> Result<()> { + deep_link.register(protocol) +} + +#[command] +pub(crate) async fn unregister( + _app: AppHandle, + _window: Window, + deep_link: State<'_, DeepLink>, + protocol: String, +) -> Result<()> { + deep_link.unregister(protocol) +} + +#[command] +pub(crate) async fn is_registered( + _app: AppHandle, + _window: Window, + deep_link: State<'_, DeepLink>, + protocol: String, +) -> Result { + deep_link.is_registered(protocol) +} diff --git a/plugins/deep-link/src/config.rs b/plugins/deep-link/src/config.rs new file mode 100644 index 0000000..49de68e --- /dev/null +++ b/plugins/deep-link/src/config.rs @@ -0,0 +1,128 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +// This module is also imported in build.rs! + +use serde::{Deserialize, Deserializer}; +use tauri_utils::config::DeepLinkProtocol; + +#[derive(Deserialize, Clone)] +pub struct AssociatedDomain { + #[serde(default = "default_schemes")] + pub scheme: Vec, + #[serde(default, deserialize_with = "deserialize_associated_host")] + pub host: Option, // Optional custom uri schemes dont NEED a host (may have one still), but required for https/http schemes + #[serde(default)] + pub path: Vec, + #[serde(default, alias = "path-pattern", rename = "pathPattern")] + pub path_pattern: Vec, + #[serde(default, alias = "path-prefix", rename = "pathPrefix")] + pub path_prefix: Vec, + #[serde(default, alias = "path-suffix", rename = "pathSuffix")] + pub path_suffix: Vec, + #[serde(default, alias = "app-link", rename = "appLink")] + pub app_link: Option, +} + +impl AssociatedDomain { + /// Returns true if the domain uses http or https scheme. + pub fn is_web_link(&self) -> bool { + self.scheme.iter().any(|s| s == "https" || s == "http") + } + + /// Returns true if the domain uses http or https scheme and has proper host configuration. + pub fn is_app_link(&self) -> bool { + self.app_link + .unwrap_or_else(|| self.is_web_link() && self.host.is_some()) + } + + pub fn validate(&self) -> Result<(), String> { + // Rule 1: All web links require a host. + if self.is_web_link() && self.host.is_none() { + return Err("Web link requires a host".into()); + } + + // Rule 2: If it's an App Link, ensure http(s) and host. + if self.is_app_link() { + if !self.is_web_link() { + return Err("AppLink must be a valid web link (https/http + host)".into()); + } + if self.scheme.iter().any(|s| s == "http") && !self.scheme.iter().any(|s| s == "https") + { + eprintln!("Warning: AppLink uses only 'http' — allowed on Android but not secure for production."); + } + } + + Ok(()) + } +} + +// TODO: Consider removing this in v3 +fn default_schemes() -> Vec { + vec!["https".to_string(), "http".to_string()] +} + +fn deserialize_associated_host<'de, D>(deserializer: D) -> Result, D::Error> +where + D: Deserializer<'de>, +{ + let opt = Option::::deserialize(deserializer)?; + if let Some(ref host) = opt { + if let Some((scheme, _)) = host.split_once("://") { + return Err(serde::de::Error::custom(format!( + "host `{host}` cannot start with a scheme, please remove the `{scheme}://` prefix" + ))); + } + } + Ok(opt) +} + +#[derive(Deserialize, Clone)] +pub struct Config { + /// Mobile requires `https://` urls. + #[serde(default)] + pub mobile: Vec, + /// Desktop requires urls starting with `://`. + /// These urls are also active in dev mode on Android. + #[allow(unused)] // Used in tauri-bundler + #[serde(default)] + pub desktop: DesktopProtocol, +} + +#[derive(Deserialize, Clone)] +#[serde(untagged)] +#[allow(unused)] // Used in tauri-bundler +pub enum DesktopProtocol { + One(DeepLinkProtocol), + List(Vec), +} + +impl Default for DesktopProtocol { + fn default() -> Self { + Self::List(Vec::new()) + } +} + +impl DesktopProtocol { + #[allow(dead_code)] + pub fn contains_scheme(&self, scheme: &String) -> bool { + match self { + Self::One(protocol) => protocol.schemes.contains(scheme), + Self::List(protocols) => protocols + .iter() + .any(|protocol| protocol.schemes.contains(scheme)), + } + } + + #[allow(dead_code)] + pub fn schemes(&self) -> Vec { + match self { + Self::One(protocol) => protocol.schemes.clone(), + Self::List(protocols) => protocols + .iter() + .flat_map(|protocol| protocol.schemes.clone()) + .collect(), + } + } +} diff --git a/plugins/deep-link/src/error.rs b/plugins/deep-link/src/error.rs new file mode 100644 index 0000000..7832995 --- /dev/null +++ b/plugins/deep-link/src/error.rs @@ -0,0 +1,61 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use serde::{ser::Serializer, Serialize}; + +/// Alias for a [`Result`](std::result::Result) with the error type [`Error`]. +pub type Result = std::result::Result; + +/// The error type for this plugin. +#[derive(Debug, thiserror::Error)] +pub enum Error { + /// The requested operation (usually registering or unregistering a protocol scheme at + /// runtime) is not supported on the current platform. + #[error("unsupported platform")] + UnsupportedPlatform, + /// Transparent wrapper around an [`std::io::Error`]. + #[error(transparent)] + Io(#[from] std::io::Error), + /// Transparent wrapper around a [`tauri::Error`]. + #[error(transparent)] + Tauri(#[from] tauri::Error), + /// Transparent wrapper around a [`windows_result::Error`]. Only used on Windows. + #[cfg(target_os = "windows")] + #[error(transparent)] + Windows(#[from] windows_result::Error), + /// Transparent wrapper around an [`ini::Error`], returned when reading or writing the + /// `.desktop` file used to register a protocol scheme. Only used on Linux. + #[cfg(target_os = "linux")] + #[error(transparent)] + Ini(#[from] ini::Error), + /// Transparent wrapper around an [`ini::ParseError`], returned when parsing the + /// `.desktop` file used to register a protocol scheme. Only used on Linux. + #[cfg(target_os = "linux")] + #[error(transparent)] + ParseIni(#[from] ini::ParseError), + /// Transparent wrapper around a [`tauri::plugin::mobile::PluginInvokeError`], returned + /// when the underlying mobile plugin invocation fails. Only used on mobile. + #[cfg(mobile)] + #[error(transparent)] + PluginInvoke(#[from] tauri::plugin::mobile::PluginInvokeError), +} + +// TODO(v3): change this into an error in v3, +// see . +#[inline] +#[cfg(target_os = "linux")] +pub(crate) fn inspect_command_error<'a>(command: &'a str) -> impl Fn(&std::io::Error) + 'a { + move |e| { + tracing::error!("Failed to run OS command `{command}`: {e}"); + } +} + +impl Serialize for Error { + fn serialize(&self, serializer: S) -> std::result::Result + where + S: Serializer, + { + serializer.serialize_str(self.to_string().as_ref()) + } +} diff --git a/plugins/deep-link/src/lib.rs b/plugins/deep-link/src/lib.rs new file mode 100644 index 0000000..9796060 --- /dev/null +++ b/plugins/deep-link/src/lib.rs @@ -0,0 +1,601 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +//! Set your Tauri application as the default handler for a URL, or check which URL(s) it was +//! opened with. +//! +//! On Windows and Linux, protocol schemes can additionally be registered and unregistered at +//! runtime with [`DeepLink::register`] and [`DeepLink::unregister`]. On macOS, Android and iOS +//! the schemes declared in the Tauri configuration are registered at build time instead, so +//! calling those methods returns [`Error::UnsupportedPlatform`]. + +use tauri::{ + plugin::{Builder, PluginApi, TauriPlugin}, + AppHandle, EventId, Listener, Manager, Runtime, +}; + +mod commands; +mod config; +mod error; + +pub use error::{Error, Result}; + +#[cfg(target_os = "android")] +const PLUGIN_IDENTIFIER: &str = "app.tauri.deep_link"; + +fn init_deep_link( + app: &AppHandle, + api: PluginApi>, +) -> crate::Result> { + #[cfg(target_os = "android")] + { + let _api = api; + + use tauri::{ + ipc::{Channel, InvokeResponseBody}, + Emitter, + }; + + let handle = _api.register_android_plugin(PLUGIN_IDENTIFIER, "DeepLinkPlugin")?; + + #[derive(serde::Deserialize)] + struct Event { + url: String, + } + + let app_handle = app.clone(); + handle.run_mobile_plugin::<()>( + "setEventHandler", + imp::EventHandler { + handler: Channel::new(move |event| { + let url = match event { + InvokeResponseBody::Json(payload) => { + serde_json::from_str::(&payload) + .ok() + .map(|payload| payload.url) + } + _ => None, + }; + + let _ = app_handle.emit("deep-link://new-url", vec![url]); + + Ok(()) + }), + }, + )?; + + return Ok(DeepLink { + app: app.clone(), + plugin_handle: handle, + }); + } + + #[cfg(target_os = "ios")] + return Ok(DeepLink { + app: app.clone(), + current: Default::default(), + config: api.config().clone(), + }); + + #[cfg(desktop)] + { + let args = std::env::args(); + let deep_link = DeepLink { + app: app.clone(), + current: Default::default(), + config: api.config().clone(), + }; + deep_link.handle_cli_arguments(args); + + Ok(deep_link) + } +} + +#[cfg(target_os = "android")] +mod imp { + use tauri::{ipc::Channel, plugin::PluginHandle, AppHandle, Runtime}; + + use serde::{Deserialize, Serialize}; + + #[derive(Serialize)] + #[serde(rename_all = "camelCase")] + pub struct EventHandler { + pub handler: Channel, + } + + #[derive(Debug, Deserialize)] + #[serde(rename_all = "camelCase")] + pub struct GetCurrentResponse { + pub url: Option, + } + + /// Access to the deep-link APIs. + pub struct DeepLink { + pub(crate) app: AppHandle, + pub(crate) plugin_handle: PluginHandle, + } + + impl DeepLink { + /// Get the current URLs that triggered the deep link. Use this on app load to check whether your app was started via a deep link. + /// + /// ## Platform-specific: + /// + /// - **Windows / Linux**: This function reads the command line arguments and checks if there's only one value, which must be an URL with scheme matching one of the configured values. + /// Note that you must manually check the arguments when registering deep link schemes dynamically with [`Self::register`]. + /// Additionally, the deep link might have been provided as a CLI argument so you should check if its format matches what you expect. + pub fn get_current(&self) -> crate::Result>> { + self.plugin_handle + .run_mobile_plugin::("getCurrent", ()) + .map(|v| v.url.map(|url| vec![url])) + .map_err(Into::into) + } + + /// Register the app as the default handler for the specified protocol. + /// + /// - `protocol`: The name of the protocol without `://`. For example, if you want your app to handle `tauri://` links, call this method with `tauri` as the protocol. + /// + /// ## Platform-specific: + /// + /// - **macOS / Android / iOS**: Unsupported, will return [`Error::UnsupportedPlatform`](`crate::Error::UnsupportedPlatform`). + pub fn register>(&self, _protocol: S) -> crate::Result<()> { + Err(crate::Error::UnsupportedPlatform) + } + + /// Unregister the app as the default handler for the specified protocol. + /// + /// - `protocol`: The name of the protocol without `://`. + /// + /// ## Platform-specific: + /// + /// - **Linux**: Can only unregister the scheme if it was initially registered with [`register`](`Self::register`). Needs the `update-desktop-database` command available on the system. May not work on older distros. + /// - **macOS / Android / iOS**: Unsupported, will return [`Error::UnsupportedPlatform`](`crate::Error::UnsupportedPlatform`). + pub fn unregister>(&self, _protocol: S) -> crate::Result<()> { + Err(crate::Error::UnsupportedPlatform) + } + + /// Check whether the app is the default handler for the specified protocol. + /// + /// - `protocol`: The name of the protocol without `://`. + /// + /// ## Platform-specific: + /// + /// - **macOS / Android / iOS**: Unsupported, will return [`Error::UnsupportedPlatform`](`crate::Error::UnsupportedPlatform`). + pub fn is_registered>(&self, _protocol: S) -> crate::Result { + Err(crate::Error::UnsupportedPlatform) + } + } +} + +#[cfg(not(target_os = "android"))] +mod imp { + use std::sync::Mutex; + #[cfg(target_os = "linux")] + use std::{ + fs::{create_dir_all, File}, + io::Write, + process::Command, + }; + #[cfg(target_os = "linux")] + use tauri::Manager; + use tauri::{AppHandle, Runtime}; + #[cfg(windows)] + use windows_registry::{CLASSES_ROOT, CURRENT_USER, LOCAL_MACHINE}; + + /// Access to the deep-link APIs. + pub struct DeepLink { + pub(crate) app: AppHandle, + pub(crate) current: Mutex>>, + pub(crate) config: Option, + } + + impl DeepLink { + /// Checks if the provided list of arguments (which should match [`std::env::args`]) + /// contains a deep link argument (for Linux and Windows). + /// + /// On Linux and Windows the deep links trigger a new app instance with the deep link URL as its only argument. + /// + /// This function does what it can to verify if the argument is actually a deep link, though it could also be a regular CLI argument. + /// To enhance its checks, we only match deep links against the schemes defined in the Tauri configuration + /// i.e. dynamic schemes WON'T be processed. + /// + /// This function updates the [`Self::get_current`] value and emits a `deep-link://new-url` event. + #[cfg(desktop)] + pub fn handle_cli_arguments, I: Iterator>(&self, mut args: I) { + use tauri::Emitter; + + let Some(config) = &self.config else { + return; + }; + + if cfg!(windows) || cfg!(target_os = "linux") { + args.next(); // bin name + let arg = args.next(); + + let maybe_deep_link = args.next().is_none(); // single argument + if !maybe_deep_link { + return; + } + + if let Some(url) = arg.and_then(|arg| arg.as_ref().parse::().ok()) { + if config.desktop.contains_scheme(&url.scheme().to_string()) { + let mut current = self.current.lock().unwrap(); + current.replace(vec![url.clone()]); + let _ = self.app.emit("deep-link://new-url", vec![url]); + } else if cfg!(debug_assertions) { + tracing::warn!("argument {url} does not match any configured deep link scheme; skipping it"); + } + } + } + } + + /// Get the current URLs that triggered the deep link. Use this on app load to check whether your app was started via a deep link. + /// + /// ## Platform-specific: + /// + /// - **Windows / Linux**: This function reads the command line arguments and checks if there's only one value, which must be an URL with scheme matching one of the configured values. + /// Note that you must manually check the arguments when registering deep link schemes dynamically with [`Self::register`]. + /// Additionally, the deep link might have been provided as a CLI argument so you should check if its format matches what you expect. + pub fn get_current(&self) -> crate::Result>> { + return Ok(self.current.lock().unwrap().clone()); + } + + /// Registers all schemes defined in the configuration file. + /// + /// This is useful to ensure the schemes are registered even if the user did not install the app properly + /// (e.g. an AppImage that was not properly registered with an AppImage launcher). + pub fn register_all(&self) -> crate::Result<()> { + let Some(config) = &self.config else { + return Ok(()); + }; + + for scheme in config.desktop.schemes() { + self.register(scheme)?; + } + + Ok(()) + } + + /// Register the app as the default handler for the specified protocol. + /// + /// - `protocol`: The name of the protocol without `://`. For example, if you want your app to handle `tauri://` links, call this method with `tauri` as the protocol. + /// + /// ## Platform-specific: + /// + /// - **Linux**: Needs the `xdg-mime` and `update-desktop-database` commands available on the system. + /// - **macOS / Android / iOS**: Unsupported, will return [`Error::UnsupportedPlatform`](`crate::Error::UnsupportedPlatform`). + pub fn register>(&self, _protocol: S) -> crate::Result<()> { + #[cfg(windows)] + { + let protocol = _protocol.as_ref(); + let key_base = format!("Software\\Classes\\{protocol}"); + + let exe = dunce::simplified(&tauri::utils::platform::current_exe()?) + .display() + .to_string(); + + let key_reg = CURRENT_USER.create(&key_base)?; + key_reg.set_string("", format!("URL:{} protocol", self.app.config().identifier))?; + key_reg.set_string("URL Protocol", "")?; + + let icon_reg = CURRENT_USER.create(format!("{key_base}\\DefaultIcon"))?; + icon_reg.set_string("", format!("{exe},0"))?; + + let cmd_reg = CURRENT_USER.create(format!("{key_base}\\shell\\open\\command"))?; + + cmd_reg.set_string("", format!("\"{exe}\" \"%1\""))?; + + Ok(()) + } + + #[cfg(target_os = "linux")] + { + let bin = tauri::utils::platform::current_exe()?; + let file_name = format!( + "{}-handler.desktop", + bin.file_name().unwrap().to_string_lossy() + ); + let appimage = self.app.env().appimage; + let exec = appimage + .clone() + .unwrap_or_else(|| bin.into_os_string()) + .to_string_lossy() + .to_string(); + let qualified_exec = format!("\"{}\" %u", exec); + + let target = self.app.path().data_dir()?.join("applications"); + + create_dir_all(&target)?; + + let target_file = target.join(&file_name); + + let mime_type = format!("x-scheme-handler/{}", _protocol.as_ref()); + + if let Ok(mut desktop_file) = ini::Ini::load_from_file(&target_file) { + if let Some(section) = desktop_file.section_mut(Some("Desktop Entry")) { + let old_mimes = section.remove("MimeType").unwrap_or_default(); + let mut change = false; + + // if the mime type is not present, append it to the list + if !old_mimes.split(';').any(|mime| mime == mime_type) { + section.append("MimeType", format!("{mime_type};{old_mimes}")); + change = true; + } else { + section.insert("MimeType".to_string(), old_mimes); + } + + // if the exec command doesnt match, update to the new one + let old_exec = section.remove("Exec").unwrap_or_default(); + if old_exec != qualified_exec { + section.append("Exec", qualified_exec); + change = true; + } else { + section.insert("Exec".to_string(), old_exec.to_string()); + } + + // if any property has changed, rewrite the .desktop file + if change { + desktop_file.write_to_file(&target_file)?; + } + } + } else { + let mut file = File::create(target_file)?; + file.write_all( + format!( + include_str!("template.desktop"), + name = self + .app + .config() + .product_name + .clone() + .unwrap_or_else(|| file_name.clone()), + qualified_exec = qualified_exec, + mime_type = mime_type + ) + .as_bytes(), + )?; + } + + Command::new("update-desktop-database") + .arg(target) + .status() + .inspect_err(crate::error::inspect_command_error( + "update-desktop-database", + ))?; + + Command::new("xdg-mime") + .args(["default", &file_name, mime_type.as_str()]) + .status() + .inspect_err(crate::error::inspect_command_error("xdg-mime"))?; + + Ok(()) + } + + #[cfg(not(any(windows, target_os = "linux")))] + Err(crate::Error::UnsupportedPlatform) + } + + /// Unregister the app as the default handler for the specified protocol. + /// + /// - `protocol`: The name of the protocol without `://`. + /// + /// ## Platform-specific: + /// + /// - **Windows**: Requires admin rights if the protocol is registered on local machine + /// (this can happen when registered from the NSIS installer when the install mode is set to both or per machine) + /// - **Linux**: Can only unregister the scheme if it was initially registered with [`register`](`Self::register`). Refreshes the desktop database with the `update-desktop-database` command; without it, [`is_registered`](`Self::is_registered`) may keep returning `true`. May not work on older distros. + /// - **macOS / Android / iOS**: Unsupported, will return [`Error::UnsupportedPlatform`](`crate::Error::UnsupportedPlatform`). + pub fn unregister>(&self, _protocol: S) -> crate::Result<()> { + #[cfg(windows)] + { + let protocol = _protocol.as_ref(); + let path = format!("Software\\Classes\\{protocol}"); + if LOCAL_MACHINE.open(&path).is_ok() { + LOCAL_MACHINE.remove_tree(&path)?; + } + if CURRENT_USER.open(&path).is_ok() { + CURRENT_USER.remove_tree(&path)?; + } + Ok(()) + } + + #[cfg(target_os = "linux")] + { + let file_name = format!( + "{}-handler.desktop", + tauri::utils::platform::current_exe()? + .file_name() + .unwrap() + .to_string_lossy() + ); + let mime_type = format!("x-scheme-handler/{}", _protocol.as_ref()); + + // stop being the default handler + let mimeapps_path = self.app.path().config_dir()?.join("mimeapps.list"); + if mimeapps_path.exists() { + let mut mimeapps = ini::Ini::load_from_file(&mimeapps_path)?; + if let Some(section) = mimeapps.section_mut(Some("Default Applications")) { + if section.get(&mime_type).unwrap_or_default() == file_name { + section.remove(&mime_type); + } + } + mimeapps.write_to_file(&mimeapps_path)?; + } + + // Stop declaring the scheme in the handler's `.desktop` file too: the desktop + // database indexes it, and with no default set `xdg-mime` falls back to that + // index, so the app would otherwise still be the handler. + let applications = self.app.path().data_dir()?.join("applications"); + let desktop_file_path = applications.join(&file_name); + // Only the `MimeType` key is touched: the file may carry other changes. + if let Ok(mut desktop_file) = ini::Ini::load_from_file(&desktop_file_path) { + if let Some(section) = desktop_file.section_mut(Some("Desktop Entry")) { + let mime_types = section + .get("MimeType") + .unwrap_or_default() + .split(';') + .filter(|mime| !mime.is_empty() && *mime != mime_type) + .map(ToString::to_string) + .collect::>(); + if mime_types.is_empty() { + section.remove("MimeType"); + } else { + section.insert("MimeType", mime_types.join(";")); + } + } + desktop_file.write_to_file(&desktop_file_path)?; + + // Without the refreshed index `xdg-mime` may keep reporting the app as the + // handler, but the scheme is unregistered as far as the app can tell, so a + // missing command is not an error. + if let Err(e) = Command::new("update-desktop-database") + .arg(&applications) + .status() + { + tracing::warn!( + "Failed to run OS command `update-desktop-database`, the desktop database may still list the app as the `{mime_type}` handler: {e}" + ); + } + } + + Ok(()) + } + + #[cfg(not(any(windows, target_os = "linux")))] + Err(crate::Error::UnsupportedPlatform) + } + + /// Check whether the app is the default handler for the specified protocol. + /// + /// - `protocol`: The name of the protocol without `://`. + /// + /// ## Platform-specific: + /// + /// - **Linux**: Needs the `xdg-mime` command available on the system. + /// - **macOS / Android / iOS**: Unsupported, will return [`Error::UnsupportedPlatform`](`crate::Error::UnsupportedPlatform`). + pub fn is_registered>(&self, _protocol: S) -> crate::Result { + #[cfg(windows)] + { + let protocol = _protocol.as_ref(); + let Ok(cmd_reg) = CLASSES_ROOT.open(format!("{protocol}\\shell\\open\\command")) + else { + return Ok(false); + }; + + let registered_cmd = cmd_reg.get_string("")?; + + let exe = dunce::simplified(&tauri::utils::platform::current_exe()?) + .display() + .to_string(); + + Ok(registered_cmd == format!("\"{exe}\" \"%1\"")) + } + #[cfg(target_os = "linux")] + { + let file_name = format!( + "{}-handler.desktop", + tauri::utils::platform::current_exe()? + .file_name() + .unwrap() + .to_string_lossy() + ); + + let output = Command::new("xdg-mime") + .args([ + "query", + "default", + &format!("x-scheme-handler/{}", _protocol.as_ref()), + ]) + .output() + .inspect_err(crate::error::inspect_command_error("xdg-mime"))?; + + Ok(String::from_utf8_lossy(&output.stdout).contains(&file_name)) + } + + #[cfg(not(any(windows, target_os = "linux")))] + Err(crate::Error::UnsupportedPlatform) + } + } +} + +pub use imp::DeepLink; +use url::Url; + +/// Extensions to [`tauri::App`], [`tauri::AppHandle`], [`tauri::WebviewWindow`], [`tauri::Webview`] and [`tauri::Window`] to access the deep-link APIs. +pub trait DeepLinkExt { + /// Returns a reference to the [`DeepLink`] API. + fn deep_link(&self) -> &DeepLink; +} + +impl> crate::DeepLinkExt for T { + fn deep_link(&self) -> &DeepLink { + self.state::>().inner() + } +} + +/// Event that is triggered when the app was requested to open a new URL. +/// +/// Typed [`tauri::Event`]. +pub struct OpenUrlEvent { + id: EventId, + urls: Vec, +} + +impl OpenUrlEvent { + /// The event ID which can be used to stop listening to the event via [`tauri::Listener::unlisten`]. + pub fn id(&self) -> EventId { + self.id + } + + /// The event URLs. + pub fn urls(self) -> Vec { + self.urls + } +} + +impl DeepLink { + /// Helper function for the `deep-link://new-url` event to run a function each time the protocol is triggered while the app is running. + /// + /// Use `get_current` on app load to check whether your app was started via a deep link. + pub fn on_open_url(&self, f: F) -> EventId { + let event_id = self.app.listen("deep-link://new-url", move |event| { + if let Ok(urls) = serde_json::from_str(event.payload()) { + f(OpenUrlEvent { + id: event.id(), + urls, + }) + } + }); + + event_id + } +} + +/// Initializes the plugin. +pub fn init() -> TauriPlugin> { + Builder::new("deep-link") + .invoke_handler(tauri::generate_handler![ + commands::get_current, + commands::register, + commands::unregister, + commands::is_registered + ]) + .setup(|app, api| { + app.manage(init_deep_link(app, api)?); + Ok(()) + }) + .on_event(|_app, _event| { + #[cfg(any(target_os = "macos", target_os = "ios"))] + if let tauri::RunEvent::Opened { urls } = _event { + use tauri::Emitter; + + let _ = _app.emit("deep-link://new-url", urls); + _app.state::>() + .current + .lock() + .unwrap() + .replace(urls.clone()); + } + }) + .build() +} diff --git a/plugins/deep-link/src/template.desktop b/plugins/deep-link/src/template.desktop new file mode 100644 index 0000000..068beb1 --- /dev/null +++ b/plugins/deep-link/src/template.desktop @@ -0,0 +1,7 @@ +[Desktop Entry] +Type=Application +Name={name} +Exec={qualified_exec} +Terminal=false +MimeType={mime_type} +NoDisplay=true diff --git a/plugins/deep-link/tsconfig.json b/plugins/deep-link/tsconfig.json new file mode 100644 index 0000000..4374468 --- /dev/null +++ b/plugins/deep-link/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "rootDir": "guest-js" + }, + "include": ["guest-js/*.ts"] +} diff --git a/plugins/dialog/CHANGELOG.md b/plugins/dialog/CHANGELOG.md new file mode 100644 index 0000000..4af3e7d --- /dev/null +++ b/plugins/dialog/CHANGELOG.md @@ -0,0 +1,341 @@ +# Changelog + +## [2.7.3] + +- [`db9c5998`](https://github.com/tauri-apps/plugins-workspace/commit/db9c5998feff9384f9cbbefcbe0d45937c00a1fc) ([#3531](https://github.com/tauri-apps/plugins-workspace/pull/3531) by [@Legend-Master](https://github.com/tauri-apps/plugins-workspace/../../Legend-Master)) Fix missing `consumer-rules.pro` on Gradle v9. +- [`cc9ec9b4`](https://github.com/tauri-apps/plugins-workspace/commit/cc9ec9b4ad2f9ec9bd57c3503ded1bd94d092c48) ([#3519](https://github.com/tauri-apps/plugins-workspace/pull/3519) by [@purvsinojiya-inventyv](https://github.com/tauri-apps/plugins-workspace/../../purvsinojiya-inventyv)) Fix Android save dialog MIME type when a single MIME type is provided. +- [`f8053e65`](https://github.com/tauri-apps/plugins-workspace/commit/f8053e659e4ccd85c1f52833411ff8417cbc5e69) ([#3527](https://github.com/tauri-apps/plugins-workspace/pull/3527) by [@Legend-Master](https://github.com/tauri-apps/plugins-workspace/../../Legend-Master)) Documented Cargo feature flags in each plugin's crate-level documentation. + +### Dependencies + +- Upgraded to `fs@2.5.2` + +## \[2.7.2] + +- [`40ae0a7f`](https://github.com/tauri-apps/plugins-workspace/commit/40ae0a7fa0ecc9b0cdbd952297f7a17f37308229) ([#3491](https://github.com/tauri-apps/plugins-workspace/pull/3491) by [@Legend-Master](https://github.com/tauri-apps/plugins-workspace/../../Legend-Master)) Use `com.google.android.material.dialog.MaterialAlertDialogBuilder` instead of `AlertDialog` so the dialog follows the app's theme + +## \[2.7.1] + +### Dependencies + +- Upgraded to `fs-js@2.5.1` + +## \[2.7.0] + +- [`24154472`](https://github.com/tauri-apps/plugins-workspace/commit/24154472a6710a690173df0a121125d1f1b871e8) ([#3287](https://github.com/tauri-apps/plugins-workspace/pull/3287) by [@Legend-Master](https://github.com/tauri-apps/plugins-workspace/../../Legend-Master)) Re-use `message` command in Rust side for `ask` and `confirm` commands, `allow-ask` and `allow-confirm` permissions are now aliases to `allow-message` + +### Dependencies + +- Upgraded to `fs-js@2.5.0` + +## \[2.6.0] + +- [`d7a0bb32`](https://github.com/tauri-apps/plugins-workspace/commit/d7a0bb325dad919d6cc132eb3898c33540de77c4) ([#3136](https://github.com/tauri-apps/plugins-workspace/pull/3136) by [@onehumandev](https://github.com/tauri-apps/plugins-workspace/../../onehumandev)) Add `fileAccessMode` option to file picker. + +## \[2.5.0] + +- [`dff6fa98`](https://github.com/tauri-apps/plugins-workspace/commit/dff6fa986a9a05ba98b6ca660fea78ae97251fc2) ([#3034](https://github.com/tauri-apps/plugins-workspace/pull/3034) by [@onehumandev](https://github.com/tauri-apps/plugins-workspace/../../onehumandev)) Add `pickerMode` option to file picker (currently only used on iOS) + +### Dependencies + +- Upgraded to `fs-js@2.4.5` + +### feat + +- [`c23fa03f`](https://github.com/tauri-apps/plugins-workspace/commit/c23fa03f07d5c1c220bcf0bca482364513e3f754) ([#3098](https://github.com/tauri-apps/plugins-workspace/pull/3098) by [@Lepidopteran](https://github.com/tauri-apps/plugins-workspace/../../Lepidopteran)) Add `xdg-portal` as an optional feature for `rfd` + +## \[2.4.2] + +- [`93426f85`](https://github.com/tauri-apps/plugins-workspace/commit/93426f85120f49beb9f40222bff45185a32d54a9) Fixed an issue that caused docs.rs builds to fail. No user facing changes. + +### Dependencies + +- Upgraded to `fs-js@2.4.4` + +## \[2.4.1] + +- [`6c9b61fb`](https://github.com/tauri-apps/plugins-workspace/commit/6c9b61fb658145d13893626112fc489f7458aa17) ([#3039](https://github.com/tauri-apps/plugins-workspace/pull/3039) by [@FabianLars](https://github.com/tauri-apps/plugins-workspace/../../FabianLars)) On Android, updated compileSdk to 36. + +### Dependencies + +- Upgraded to `fs-js@2.4.3` + +## \[2.4.0] + +- [`509eba8d`](https://github.com/tauri-apps/plugins-workspace/commit/509eba8d441c4f6ecf0af77b572cb2afd69a752d) ([#2641](https://github.com/tauri-apps/plugins-workspace/pull/2641) by [@amrbashir](https://github.com/tauri-apps/plugins-workspace/../../amrbashir)) Add support for showing a message dialog with 3 buttons. + +## \[2.3.3] + +### Dependencies + +- Upgraded to `fs-js@2.4.2` + +## \[2.3.2] + +- [`af08c66f`](https://github.com/tauri-apps/plugins-workspace/commit/af08c66faafe0dffc4b0a80aef030cd3f0f89a9c) ([#2871](https://github.com/tauri-apps/plugins-workspace/pull/2871) by [@FabianLars](https://github.com/tauri-apps/plugins-workspace/../../FabianLars)) Fixed an issue that caused the file picker not to open on Android when extension filters were set. + +## \[2.3.1] + +### Dependencies + +- Upgraded to `fs-js@2.4.1` + +## \[2.3.0] + +- [`f209b2f2`](https://github.com/tauri-apps/plugins-workspace/commit/f209b2f23cb29133c97ad5961fb46ef794dbe063) ([#2804](https://github.com/tauri-apps/plugins-workspace/pull/2804) by [@renovate](https://github.com/tauri-apps/plugins-workspace/../../renovate)) Updated tauri to 2.6 + +### Dependencies + +- Upgraded to `fs-js@2.4.0` + +## \[2.2.2] + +### Dependencies + +- Upgraded to `fs-js@2.3.0` + +## \[2.2.1] + +### Dependencies + +- Upgraded to `fs-js@2.2.1` + +## \[2.2.0] + +- [`3a79266b`](https://github.com/tauri-apps/plugins-workspace/commit/3a79266b8cf96a55b1ae6339d725567d45a44b1d) ([#2173](https://github.com/tauri-apps/plugins-workspace/pull/2173) by [@FabianLars](https://github.com/tauri-apps/plugins-workspace/../../FabianLars)) Bumped all plugins to `v2.2.0`. From now, the versions for the Rust and JavaScript packages of each plugin will be in sync with each other. + +### Dependencies + +- Upgraded to `fs-js@2.1.0` + +## \[2.0.2] + +### Dependencies + +- Upgraded to `fs-js@2.0.4` + +## \[2.0.4] + +- [`76f99ce9`](https://github.com/tauri-apps/plugins-workspace/commit/76f99ce999a2ff9e40235c1675e3eb6570b5e1e2) ([#2108](https://github.com/tauri-apps/plugins-workspace/pull/2108) by [@FabianLars](https://github.com/tauri-apps/plugins-workspace/../../FabianLars)) The `Dialog` struct is now correctly exported, primarily to fix the documentation on `docs.rs`. + +### Dependencies + +- Upgraded to `fs@2.1.0` + +## \[2.0.3] + +### Dependencies + +- Upgraded to `fs@2.0.3` + +## \[2.0.1] + +- [`2302c2db`](https://github.com/tauri-apps/plugins-workspace/commit/2302c2db1c49673e61dcbda8cdb01b2c57e9ba6f) ([#1910](https://github.com/tauri-apps/plugins-workspace/pull/1910) by [@Legend-Master](https://github.com/tauri-apps/plugins-workspace/../../Legend-Master)) Fix `ask` and `confirm` not using system button texts +- [`aee14ed4`](https://github.com/tauri-apps/plugins-workspace/commit/aee14ed4261cdedc4ed7cc2686f01f437859a5c7) ([#1892](https://github.com/tauri-apps/plugins-workspace/pull/1892) by [@nashaofu](https://github.com/tauri-apps/plugins-workspace/../../nashaofu)) Set `save` dialog mime type from the `filters` extensions on Android. + +## \[2.0.1] + +- [`a1a82208`](https://github.com/tauri-apps/plugins-workspace/commit/a1a82208ed4ab87f83310be0dc95428aec9ab241) ([#1873](https://github.com/tauri-apps/plugins-workspace/pull/1873) by [@lucasfernog](https://github.com/tauri-apps/plugins-workspace/../../lucasfernog)) Downgrade MSRV to 1.77.2 to support Windows 7. + +### Dependencies + +- Upgraded to `fs@2.0.1` + +## \[2.0.0] + +- [`e2c4dfb6`](https://github.com/tauri-apps/plugins-workspace/commit/e2c4dfb6af43e5dd8d9ceba232c315f5febd55c1) Update to tauri v2 stable release. + +### Dependencies + +- Upgraded to `fs@2.0.0` + +## \[2.0.0-rc.8] + +- [`6bf1bd8d`](https://github.com/tauri-apps/plugins-workspace/commit/6bf1bd8d44bb95618590aa066e638509b014e0f9) ([#1805](https://github.com/tauri-apps/plugins-workspace/pull/1805) by [@renovate](https://github.com/tauri-apps/plugins-workspace/../../renovate)) Update rfd to 0.15 + +### Dependencies + +- Upgraded to `fs@2.0.0-rc.6` + +### breaking + +- [`04459afb`](https://github.com/tauri-apps/plugins-workspace/commit/04459afbb67aafa5cd57e6a148c2beb0a8d3e04a) ([#1842](https://github.com/tauri-apps/plugins-workspace/pull/1842) by [@Legend-Master](https://github.com/tauri-apps/plugins-workspace/../../Legend-Master)) Changed `MessageDialogBuilder::ok_button_label` and `MessageDialogBuilder::cancel_button_label` to `MessageDialogBuilder::buttons` which takes an enum now + +## \[2.0.0-rc.7] + +### Dependencies + +- Upgraded to `fs@2.0.0-rc.5` + +## \[2.0.0-rc.6] + +- [`2b898f07`](https://github.com/tauri-apps/plugins-workspace/commit/2b898f078688c57309ca17962bf02e665c406514) ([#1769](https://github.com/tauri-apps/plugins-workspace/pull/1769) by [@lucasfernog](https://github.com/tauri-apps/plugins-workspace/../../lucasfernog)) Update Tauri scopes (asset protocol) when using the `open()` command to select directories. + +### Dependencies + +- Upgraded to `fs@2.0.0-rc.4` + +## \[2.0.0-rc.5] + +- [`a2fe5551`](https://github.com/tauri-apps/plugins-workspace/commit/a2fe55512f908dd11c814ce021d164f01677572a) ([#1727](https://github.com/tauri-apps/plugins-workspace/pull/1727) by [@amrbashir](https://github.com/tauri-apps/plugins-workspace/../../amrbashir)) Add utility methods on `FilePath` and `SafeFilePath` enums which are: + + - `path` + - `simplified` + - `into_path` +- [`a2fe5551`](https://github.com/tauri-apps/plugins-workspace/commit/a2fe55512f908dd11c814ce021d164f01677572a) ([#1727](https://github.com/tauri-apps/plugins-workspace/pull/1727) by [@amrbashir](https://github.com/tauri-apps/plugins-workspace/../../amrbashir)) Implement `Serialize`, `Deserialize`, `From`, `TryFrom` and `FromStr` traits for `FilePath` and `SafeFilePath` enums. +- [`a2fe5551`](https://github.com/tauri-apps/plugins-workspace/commit/a2fe55512f908dd11c814ce021d164f01677572a) ([#1727](https://github.com/tauri-apps/plugins-workspace/pull/1727) by [@amrbashir](https://github.com/tauri-apps/plugins-workspace/../../amrbashir)) Mark `Error` enum as `#[non_exhuastive]`. +- [`a2fe5551`](https://github.com/tauri-apps/plugins-workspace/commit/a2fe55512f908dd11c814ce021d164f01677572a) ([#1727](https://github.com/tauri-apps/plugins-workspace/pull/1727) by [@amrbashir](https://github.com/tauri-apps/plugins-workspace/../../amrbashir)) Add `SafeFilePath` enum. + +### Dependencies + +- Upgraded to `fs@2.0.0-rc.3` + +## \[2.0.0-rc.4] + +### Dependencies + +- Upgraded to `fs@2.0.0-rc.2` + +### breaking + +- [`0cb99bda`](https://github.com/tauri-apps/plugins-workspace/commit/0cb99bdaf11b5a9bb66b80bdf40b085d87c3066d) ([#1706](https://github.com/tauri-apps/plugins-workspace/pull/1706) by [@lucasfernog](https://github.com/tauri-apps/plugins-workspace/../../lucasfernog)) If no filters are specified, the file picker dialog now defaults to a file selection instead of photos. + +### feat + +- [`feb1e93f`](https://github.com/tauri-apps/plugins-workspace/commit/feb1e93fcb9a913c002daa29e3b709f24b97c664) ([#1707](https://github.com/tauri-apps/plugins-workspace/pull/1707) by [@lucasfernog](https://github.com/tauri-apps/plugins-workspace/../../lucasfernog)) Implement `save` API on iOS. + +## \[2.0.0-rc.1] + +- [`448846b8`](https://github.com/tauri-apps/plugins-workspace/commit/448846b834d23df6e7c5dc66c5dd9aa0cb01846d) ([#1658](https://github.com/tauri-apps/plugins-workspace/pull/1658) by [@mikoto2000](https://github.com/tauri-apps/plugins-workspace/../../mikoto2000)) The `open` function now returns a string representing either the file path or URI instead of an object. + To read the file data, use the `fs` APIs. +- [`e2e97db5`](https://github.com/tauri-apps/plugins-workspace/commit/e2e97db51983267f5be84d4f6f0278d58834d1f5) ([#1701](https://github.com/tauri-apps/plugins-workspace/pull/1701) by [@lucasfernog](https://github.com/tauri-apps/plugins-workspace/../../lucasfernog)) Update to tauri 2.0.0-rc.8 + +## \[2.0.0-rc.2] + +- [`b9147758`](https://github.com/tauri-apps/plugins-workspace/commit/b914775898c2bee7ceb20bd17ee595005cd17a64) ([#1679](https://github.com/tauri-apps/plugins-workspace/pull/1679) by [@lucasfernog](https://github.com/tauri-apps/plugins-workspace/../../lucasfernog)) Explicitly set a minimum macOS version for the Swift package. + +## \[2.0.0-rc.1] + +### feat + +- [`bc7eecf4`](https://github.com/tauri-apps/plugins-workspace/commit/bc7eecf4202e7d23b987440c1cbd2da0f68c8bef) ([#1657](https://github.com/tauri-apps/plugins-workspace/pull/1657) by [@mikoto2000](https://github.com/tauri-apps/plugins-workspace/../../mikoto2000)) Implement `save` API on Android. + +### changes + +- [`6b079cfd`](https://github.com/tauri-apps/plugins-workspace/commit/6b079cfdd107c94abc2c7300f6af00bac3ff4040) ([#1649](https://github.com/tauri-apps/plugins-workspace/pull/1649) by [@ahqsoftwares](https://github.com/tauri-apps/plugins-workspace/../../ahqsoftwares)) Remove targetSdk from build.kts files as it is deprecated and will be removed from DSL v9.0 + +## \[2.0.0-rc.0] + +- [`9887d1`](https://github.com/tauri-apps/plugins-workspace/commit/9887d14bd0e971c4c0f5c1188fc4005d3fc2e29e) Update to tauri RC. + +## \[2.0.0-beta.8] + +- [`99d6ac0f`](https://github.com/tauri-apps/plugins-workspace/commit/99d6ac0f9506a6a4a1aa59c728157190a7441af6) ([#1606](https://github.com/tauri-apps/plugins-workspace/pull/1606) by [@FabianLars](https://github.com/tauri-apps/plugins-workspace/../../FabianLars)) The JS packages now specify the *minimum* `@tauri-apps/api` version instead of a single exact version. +- [`6de87966`](https://github.com/tauri-apps/plugins-workspace/commit/6de87966ecc00ad9d91c25be452f1f46bd2b7e1f) ([#1597](https://github.com/tauri-apps/plugins-workspace/pull/1597) by [@Legend-Master](https://github.com/tauri-apps/plugins-workspace/../../Legend-Master)) Update to tauri beta.25. + +## \[2.0.0-beta.7] + +- [`22a17980`](https://github.com/tauri-apps/plugins-workspace/commit/22a17980ff4f6f8c40adb1b8f4ffc6dae2fe7e30) ([#1537](https://github.com/tauri-apps/plugins-workspace/pull/1537) by [@lucasfernog](https://github.com/tauri-apps/plugins-workspace/../../lucasfernog)) Update to tauri beta.24. + +## \[2.0.0-beta.6] + +- [`76daee7a`](https://github.com/tauri-apps/plugins-workspace/commit/76daee7aafece34de3092c86e531cf9eb1138989) ([#1512](https://github.com/tauri-apps/plugins-workspace/pull/1512) by [@renovate](https://github.com/tauri-apps/plugins-workspace/../../renovate)) Update to tauri beta.23. + +## \[2.0.0-beta.5] + +- [`9013854f`](https://github.com/tauri-apps/plugins-workspace/commit/9013854f42a49a230b9dbb9d02774765528a923f)([#1382](https://github.com/tauri-apps/plugins-workspace/pull/1382)) Update to tauri beta.22. + +## \[2.0.0-beta.4] + +- [`430bd6f4`](https://github.com/tauri-apps/plugins-workspace/commit/430bd6f4f379bee5d232ae6b098ae131db7f178a)([#1363](https://github.com/tauri-apps/plugins-workspace/pull/1363)) Update to tauri beta.20. + +## \[2.0.0-beta.3] + +- [`bd1ed590`](https://github.com/tauri-apps/plugins-workspace/commit/bd1ed5903ffcce5500310dac1e59e8c67674ef1e)([#1237](https://github.com/tauri-apps/plugins-workspace/pull/1237)) Update to tauri beta.17. + +## \[2.0.0-beta.6] + +- [`326df688`](https://github.com/tauri-apps/plugins-workspace/commit/326df6883998d416fc0837583ed972854628bb52)([#1236](https://github.com/tauri-apps/plugins-workspace/pull/1236)) Fixes command argument parsing on iOS. + +### Dependencies + +- Upgraded to `fs@2.0.0-beta.6` + +## \[2.0.0-beta.5] + +- [`bb51a41`](https://github.com/tauri-apps/plugins-workspace/commit/bb51a41d67ebf989e8aedf10c4b1a7f9514d1bdf)([#1168](https://github.com/tauri-apps/plugins-workspace/pull/1168)) **Breaking Change:** All apis that return paths to the frontend will now remove the `\\?\` UNC prefix on Windows. + +### Dependencies + +- Upgraded to `fs@2.0.0-beta.5` + +## \[2.0.0-beta.4] + +- [`4cd8112`](https://github.com/tauri-apps/plugins-workspace/commit/4cd81126fdf25e1847546f8fdbd924aa4bfeabb5)([#1056](https://github.com/tauri-apps/plugins-workspace/pull/1056)) Fixed an issue where dialogs on android would return the Content URI instead of the file path + +### Dependencies + +- Upgraded to `fs@2.0.0-beta.4` + +## \[2.0.0-beta.3] + +- [`35ea595`](https://github.com/tauri-apps/plugins-workspace/commit/35ea5956d060f0bdafd140f2541c607bb811805b)([#1073](https://github.com/tauri-apps/plugins-workspace/pull/1073)) Fixed an issue where the dialog apis panicked when they were called with no application windows open. +- [`a04ea2f`](https://github.com/tauri-apps/plugins-workspace/commit/a04ea2f38294d5a3987578283badc8eec87a7752)([#1071](https://github.com/tauri-apps/plugins-workspace/pull/1071)) The global API script is now only added to the binary when the `withGlobalTauri` config is true. + +### Dependencies + +- Upgraded to `fs@2.0.0-beta.3` + +## \[2.0.0-beta.2] + +- [`aa25c91`](https://github.com/tauri-apps/plugins-workspace/commit/aa25c91bb01e957872fb2b606a3acaf2f2c4ef3a)([#978](https://github.com/tauri-apps/plugins-workspace/pull/978)) Allow configuring `canCreateDirectories` for open and save dialogs on macOS, if not configured, it will be set to `true` by default. +- [`99bea25`](https://github.com/tauri-apps/plugins-workspace/commit/99bea2559c2c0648c2519c50a18cd124dacef57b)([#1005](https://github.com/tauri-apps/plugins-workspace/pull/1005)) Update to tauri beta.8. + +## \[2.0.0-beta.1] + +- [`569defb`](https://github.com/tauri-apps/plugins-workspace/commit/569defbe9492e38938554bb7bdc1be9151456d21) Update to tauri beta.4. + +## \[2.0.0-beta.0] + +- [`d198c01`](https://github.com/tauri-apps/plugins-workspace/commit/d198c014863ee260cb0de88a14b7fc4356ef7474)([#862](https://github.com/tauri-apps/plugins-workspace/pull/862)) Update to tauri beta. + +## \[2.0.0-alpha.7] + +### Dependencies + +- Upgraded to `fs@2.0.0-alpha.7` + +## \[2.0.0-alpha.5] + +- [`387c2f9`](https://github.com/tauri-apps/plugins-workspace/commit/387c2f9e0ce4c75c07ffa3fd76391a25b58f5daf)([#802](https://github.com/tauri-apps/plugins-workspace/pull/802)) Update to @tauri-apps/api v2.0.0-alpha.13. + +## \[2.0.0-alpha.4] + +- [`387c2f9`](https://github.com/tauri-apps/plugins-workspace/commit/387c2f9e0ce4c75c07ffa3fd76391a25b58f5daf)([#802](https://github.com/tauri-apps/plugins-workspace/pull/802)) Update to @tauri-apps/api v2.0.0-alpha.12. +- [`2e2c0a1`](https://github.com/tauri-apps/plugins-workspace/commit/2e2c0a1b958fcbc7de855ef1d305b11855f2eb8e)([#769](https://github.com/tauri-apps/plugins-workspace/pull/769)) Fix incorrect result for dialog messages. + +## \[2.0.0-alpha.3] + +- [`e438e0a`](https://github.com/tauri-apps/plugins-workspace/commit/e438e0a62d4b430a5159f05f13ecd397dd891a0d)([#676](https://github.com/tauri-apps/plugins-workspace/pull/676)) Update to @tauri-apps/api v2.0.0-alpha.11. + +## \[2.0.0-alpha.2] + +- [`5c13736`](https://github.com/tauri-apps/plugins-workspace/commit/5c137365c60790e8d4037d449e8237aa3fffdab0)([#673](https://github.com/tauri-apps/plugins-workspace/pull/673)) Update to @tauri-apps/api v2.0.0-alpha.9. + +## \[2.0.0-alpha.2] + +- [`4e2cef9`](https://github.com/tauri-apps/plugins-workspace/commit/4e2cef9b702bbbb9cf4ee17de50791cb21f1b2a4)([#593](https://github.com/tauri-apps/plugins-workspace/pull/593)) Update to alpha.12. + +### Dependencies + +- Upgraded to `fs@2.0.0-alpha.2` + +## \[2.0.0-alpha.1] + +- [`d74fc0a`](https://github.com/tauri-apps/plugins-workspace/commit/d74fc0a097996e90a37be8f57d50b7d1f6ca616f)([#555](https://github.com/tauri-apps/plugins-workspace/pull/555)) Update to alpha.11. + +## \[2.0.0-alpha.0] + +- [`717ae67`](https://github.com/tauri-apps/plugins-workspace/commit/717ae670978feb4492fac1f295998b93f2b9347f)([#371](https://github.com/tauri-apps/plugins-workspace/pull/371)) First v2 alpha release! + d6e80b)([#545](https://github.com/tauri-apps/plugins-workspace/pull/545)) Fixes docs.rs build by enabling the `tauri/dox` feature flag. +g. diff --git a/plugins/dialog/Cargo.toml b/plugins/dialog/Cargo.toml new file mode 100644 index 0000000..3064b1e --- /dev/null +++ b/plugins/dialog/Cargo.toml @@ -0,0 +1,58 @@ +[package] +name = "tauri-plugin-dialog" +version = "2.7.3" +description = "Native system dialogs for opening and saving files along with message dialogs on your Tauri application." +edition = { workspace = true } +authors = { workspace = true } +license = { workspace = true } +rust-version = { workspace = true } +repository = { workspace = true } +links = "tauri-plugin-dialog" +exclude = [ + "/banner.png", + "/guest-js", + "/package.json", + "/rollup.config.js", + "/test", + "/tsconfig.json", +] + +[features] +default = ["gtk3"] +xdg-portal = ["rfd/xdg-portal", "rfd/tokio", "rfd/wayland"] +gtk3 = ["rfd/gtk3"] + +[package.metadata.docs.rs] +targets = ["x86_64-unknown-linux-gnu", "x86_64-linux-android"] + +[package.metadata.platforms.support] +windows = { level = "full", notes = "" } +linux = { level = "full", notes = "" } +macos = { level = "full", notes = "" } +android = { level = "partial", notes = "Does not support folder picker" } +ios = { level = "partial", notes = "Does not support folder picker" } + +[build-dependencies] +tauri-plugin = { workspace = true, features = ["build"] } + +[dev-dependencies] +tauri = { workspace = true, features = ["wry"] } + +[dependencies] +serde = { workspace = true } +serde_json = { workspace = true } +tauri = { workspace = true } +log = { workspace = true } +thiserror = { workspace = true } +url = { workspace = true } +tauri-plugin-fs = { path = "../fs", version = "2.5.2" } + +[target.'cfg(target_os = "ios")'.dependencies] +tauri = { workspace = true, features = ["wry"] } + +[target."cfg(any(target_os = \"macos\", windows, target_os = \"linux\", target_os = \"dragonfly\", target_os = \"freebsd\", target_os = \"openbsd\", target_os = \"netbsd\"))".dependencies] +rfd = { version = "0.16", default-features = false, features = [ + "common-controls-v6", +] } + +raw-window-handle = "0.6" diff --git a/plugins/dialog/LICENSE.spdx b/plugins/dialog/LICENSE.spdx new file mode 100644 index 0000000..cdd0df5 --- /dev/null +++ b/plugins/dialog/LICENSE.spdx @@ -0,0 +1,20 @@ +SPDXVersion: SPDX-2.1 +DataLicense: CC0-1.0 +PackageName: tauri +DataFormat: SPDXRef-1 +PackageSupplier: Organization: The Tauri Programme in the Commons Conservancy +PackageHomePage: https://tauri.app +PackageLicenseDeclared: Apache-2.0 +PackageLicenseDeclared: MIT +PackageCopyrightText: 2019-2022, The Tauri Programme in the Commons Conservancy +PackageSummary: Tauri is a rust project that enables developers to make secure +and small desktop applications using a web frontend. + +PackageComment: The package includes the following libraries; see +Relationship information. + +Created: 2019-05-20T09:00:00Z +PackageDownloadLocation: git://github.com/tauri-apps/tauri +PackageDownloadLocation: git+https://github.com/tauri-apps/tauri.git +PackageDownloadLocation: git+ssh://github.com/tauri-apps/tauri.git +Creator: Person: Daniel Thompson-Yvetot \ No newline at end of file diff --git a/plugins/dialog/LICENSE_APACHE-2.0 b/plugins/dialog/LICENSE_APACHE-2.0 new file mode 100644 index 0000000..4947287 --- /dev/null +++ b/plugins/dialog/LICENSE_APACHE-2.0 @@ -0,0 +1,177 @@ + + 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 \ No newline at end of file diff --git a/plugins/dialog/LICENSE_MIT b/plugins/dialog/LICENSE_MIT new file mode 100644 index 0000000..4d75472 --- /dev/null +++ b/plugins/dialog/LICENSE_MIT @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2017 - Present Tauri Apps 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. \ No newline at end of file diff --git a/plugins/dialog/README.md b/plugins/dialog/README.md new file mode 100644 index 0000000..e062c80 --- /dev/null +++ b/plugins/dialog/README.md @@ -0,0 +1,107 @@ +![plugin-dialog](https://github.com/tauri-apps/plugins-workspace/raw/v2/plugins/dialog/banner.png) + +Native system dialogs for opening and saving files along with message dialogs. + +| Platform | Supported | +| -------- | --------- | +| Linux | ✓ | +| Windows | ✓ | +| macOS | ✓ | +| Android | ✓ | +| iOS | ✓ | + +## Install + +_This plugin requires a Rust version of at least **1.77.2**_ + +There are three general methods of installation that we can recommend. + +1. Use crates.io and npm (easiest, and requires you to trust that our publishing pipeline worked) +2. Pull sources directly from Github using git tags / revision hashes (most secure) +3. Git submodule install this repo in your tauri project and then use file protocol to ingest the source (most secure, but inconvenient to use) + +Install the Core plugin by adding the following to your `Cargo.toml` file: + +`src-tauri/Cargo.toml` + +```toml +[dependencies] +tauri-plugin-dialog = "2.0.0" +# alternatively with Git: +tauri-plugin-dialog = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v2" } +``` + +### Linux XDG Desktop Portal Support + +By default, this plugin uses gtk to show dialogs, however since `v2.5.0` you can switch to using [XDG Desktop Portal](https://flatpak.github.io/xdg-desktop-portal/) by adding the following to your `Cargo.toml` file: + +```toml +[dependencies] +tauri-plugin-dialog = { version = "2.5.0", default-features = false, features = ["xdg-portal"] } +# alternatively with Git: +tauri-plugin-dialog = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v2", default-features = false, features = ["xdg-portal"] } + +``` + +Do note if you use the `xdg-portal` feature, you need to ensure that [`zenity`](https://gitlab.gnome.org/GNOME/zenity) and an [XDG Desktop Portal backend](https://flatpak.github.io/xdg-desktop-portal#using-portals) is installed with your program. + +For more information, see [XDG Desktop Portal documentation](https://flatpak.github.io/xdg-desktop-portal/) and [`rfd` documentation](https://docs.rs/rfd/latest/rfd#xdg-desktop-portal-backend). + +### JavaScript + +You can install the JavaScript Guest bindings using your preferred JavaScript package manager: + +```sh +pnpm add @tauri-apps/plugin-dialog +# or +npm add @tauri-apps/plugin-dialog +# or +yarn add @tauri-apps/plugin-dialog +``` + +## Usage + +First you need to register the core plugin with Tauri: + +`src-tauri/src/lib.rs` + +```rust +fn main() { + tauri::Builder::default() + .plugin(tauri_plugin_dialog::init()) + .run(tauri::generate_context!()) + .expect("error while running tauri application"); +} +``` + +Afterwards all the plugin's APIs are available through the JavaScript guest bindings: + +```javascript + +``` + +## Contributing + +PRs accepted. Please make sure to read the Contributing Guide before making a pull request. + +## Partners + + + + + + + +
+ + CrabNebula + +
+ +For the complete list of sponsors please visit our [website](https://tauri.app#sponsors) and [Open Collective](https://opencollective.com/tauri). + +## License + +Code: (c) 2015 - Present - The Tauri Programme within The Commons Conservancy. + +MIT or MIT/Apache 2.0 where applicable. diff --git a/plugins/dialog/SECURITY.md b/plugins/dialog/SECURITY.md new file mode 100644 index 0000000..4f09bba --- /dev/null +++ b/plugins/dialog/SECURITY.md @@ -0,0 +1,23 @@ +# Security Policy + +**Do not report security vulnerabilities through public GitHub issues.** + +**Please use the [Private Vulnerability Disclosure](https://docs.github.com/en/code-security/security-advisories/guidance-on-reporting-and-writing-information-about-vulnerabilities/privately-reporting-a-security-vulnerability#privately-reporting-a-security-vulnerability) feature of GitHub.** + +Include as much of the following information: + +- Type of issue (e.g. improper input parsing, privilege escalation, etc.) +- The location of the affected source code (tag/branch/commit or direct URL) +- Any special configuration required to reproduce the issue +- The distribution affected or used to help us with reproduction of the issue +- Step-by-step instructions to reproduce the issue +- Ideally a reproduction repository +- Impact of the issue, including how an attacker might exploit the issue + +We prefer to receive reports in English. + +## Contact + +Please disclose a vulnerability or security relevant issue here: [https://github.com/tauri-apps/plugins-workspace/security/advisories/new](https://github.com/tauri-apps/plugins-workspace/security/advisories/new). + +Alternatively, you can also contact us by email via [security@tauri.app](mailto:security@tauri.app). diff --git a/plugins/dialog/android/.gitignore b/plugins/dialog/android/.gitignore new file mode 100644 index 0000000..c0f21ec --- /dev/null +++ b/plugins/dialog/android/.gitignore @@ -0,0 +1,2 @@ +/build +/.tauri diff --git a/plugins/dialog/android/build.gradle.kts b/plugins/dialog/android/build.gradle.kts new file mode 100644 index 0000000..96abbf5 --- /dev/null +++ b/plugins/dialog/android/build.gradle.kts @@ -0,0 +1,35 @@ +plugins { + id("com.android.library") + id("org.jetbrains.kotlin.android") +} + +android { + namespace = "app.tauri.dialog" + compileSdk = 36 + + defaultConfig { + minSdk = 24 + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + consumerProguardFiles("consumer-rules.pro") + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 + } + kotlinOptions { + jvmTarget = "1.8" + } +} + +dependencies { + + implementation("androidx.core:core-ktx:1.9.0") + implementation("androidx.appcompat:appcompat:1.7.1") + implementation("com.google.android.material:material:1.13.0") + testImplementation("junit:junit:4.13.2") + androidTestImplementation("androidx.test.ext:junit:1.1.5") + androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1") + implementation(project(":tauri-android")) +} diff --git a/plugins/dialog/android/consumer-rules.pro b/plugins/dialog/android/consumer-rules.pro new file mode 100644 index 0000000..481bb43 --- /dev/null +++ b/plugins/dialog/android/consumer-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/plugins/dialog/android/settings.gradle b/plugins/dialog/android/settings.gradle new file mode 100644 index 0000000..14a752e --- /dev/null +++ b/plugins/dialog/android/settings.gradle @@ -0,0 +1,2 @@ +include ':tauri-android' +project(':tauri-android').projectDir = new File('./.tauri/tauri-api') diff --git a/plugins/dialog/android/src/androidTest/java/ExampleInstrumentedTest.kt b/plugins/dialog/android/src/androidTest/java/ExampleInstrumentedTest.kt new file mode 100644 index 0000000..6c9d837 --- /dev/null +++ b/plugins/dialog/android/src/androidTest/java/ExampleInstrumentedTest.kt @@ -0,0 +1,28 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +package app.tauri.dialog + +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.ext.junit.runners.AndroidJUnit4 + +import org.junit.Test +import org.junit.runner.RunWith + +import org.junit.Assert.* + +/** + * Instrumented test, which will execute on an Android device. + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +@RunWith(AndroidJUnit4::class) +class ExampleInstrumentedTest { + @Test + fun useAppContext() { + // Context of the app under test. + val appContext = InstrumentationRegistry.getInstrumentation().targetContext + assertEquals("app.tauri.dialog", appContext.packageName) + } +} diff --git a/plugins/dialog/android/src/main/AndroidManifest.xml b/plugins/dialog/android/src/main/AndroidManifest.xml new file mode 100644 index 0000000..9a40236 --- /dev/null +++ b/plugins/dialog/android/src/main/AndroidManifest.xml @@ -0,0 +1,3 @@ + + + diff --git a/plugins/dialog/android/src/main/java/DialogPlugin.kt b/plugins/dialog/android/src/main/java/DialogPlugin.kt new file mode 100644 index 0000000..b11c201 --- /dev/null +++ b/plugins/dialog/android/src/main/java/DialogPlugin.kt @@ -0,0 +1,246 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +package app.tauri.dialog + +import android.app.Activity +import android.content.Intent +import android.net.Uri +import android.os.Handler +import android.os.Looper +import android.webkit.MimeTypeMap +import androidx.activity.result.ActivityResult +import app.tauri.Logger +import app.tauri.annotation.ActivityCallback +import app.tauri.annotation.Command +import app.tauri.annotation.InvokeArg +import app.tauri.annotation.TauriPlugin +import app.tauri.plugin.Invoke +import app.tauri.plugin.JSArray +import app.tauri.plugin.JSObject +import app.tauri.plugin.Plugin +import com.google.android.material.dialog.MaterialAlertDialogBuilder + +@InvokeArg +class Filter { + lateinit var extensions: Array +} + +@InvokeArg +class FilePickerOptions { + lateinit var filters: Array + var multiple: Boolean? = null + var pickerMode: String? = null +} + +@InvokeArg +class MessageOptions { + var title: String? = null + lateinit var message: String + var okButtonLabel: String? = null + var noButtonLabel: String? = null + var cancelButtonLabel: String? = null +} + +@InvokeArg +class SaveFileDialogOptions { + var fileName: String? = null + lateinit var filters: Array +} + +@TauriPlugin +class DialogPlugin(private val activity: Activity): Plugin(activity) { + var filePickerOptions: FilePickerOptions? = null + + @Command + fun showFilePicker(invoke: Invoke) { + try { + val args = invoke.parseArgs(FilePickerOptions::class.java) + val parsedTypes = parseFiltersOption(args.filters) + + // TODO: ACTION_OPEN_DOCUMENT ?? + val intent = Intent(Intent.ACTION_GET_CONTENT) + intent.addCategory(Intent.CATEGORY_OPENABLE) + + if (args.pickerMode == "image") { + intent.type = "image/*" + } else if (args.pickerMode == "video") { + intent.type = "video/*" + } else if (args.pickerMode == "media") { + intent.type = "*/*" + intent.putExtra(Intent.EXTRA_MIME_TYPES, arrayOf("video/*", "image/*")) + } else if (parsedTypes.isNotEmpty()) { + intent.type = "*/*" + intent.putExtra(Intent.EXTRA_MIME_TYPES, parsedTypes) + } else { + intent.type = "*/*" + } + + intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, args.multiple ?: false) + + startActivityForResult(invoke, intent, "filePickerResult") + } catch (ex: Exception) { + val message = ex.message ?: "Failed to pick file" + Logger.error(message) + invoke.reject(message) + } + } + + @ActivityCallback + fun filePickerResult(invoke: Invoke, result: ActivityResult) { + try { + when (result.resultCode) { + Activity.RESULT_OK -> { + val callResult = createPickFilesResult(result.data) + invoke.resolve(callResult) + } + Activity.RESULT_CANCELED -> invoke.reject("File picker cancelled") + else -> invoke.reject("Failed to pick files") + } + } catch (ex: java.lang.Exception) { + val message = ex.message ?: "Failed to read file pick result" + Logger.error(message) + invoke.reject(message) + } + } + + private fun createPickFilesResult(data: Intent?): JSObject { + val callResult = JSObject() + if (data == null) { + callResult.put("files", null) + return callResult + } + val uris: MutableList = ArrayList() + if (data.clipData == null) { + val uri: Uri? = data.data + uris.add(uri?.toString()) + } else { + for (i in 0 until data.clipData!!.itemCount) { + val uri: Uri = data.clipData!!.getItemAt(i).uri + uris.add(uri.toString()) + } + } + callResult.put("files", JSArray.from(uris.toTypedArray())) + return callResult + } + + private fun parseFiltersOption(filters: Array): Array { + val mimeTypes = mutableListOf() + for (filter in filters) { + for (ext in filter.extensions) { + if (ext.contains('/')) { + mimeTypes.add(if (ext == "text/csv") "text/comma-separated-values" else ext) + } else { + MimeTypeMap.getSingleton().getMimeTypeFromExtension(ext)?.let { + mimeTypes.add(it) + } + } + } + } + return mimeTypes.toTypedArray() + } + + @Command + fun showMessageDialog(invoke: Invoke) { + val args = invoke.parseArgs(MessageOptions::class.java) + + if (activity.isFinishing) { + invoke.reject("App is finishing") + return + } + + val handler = { value: String -> + val ret = JSObject() + ret.put("value", value) + invoke.resolve(ret) + } + + Handler(Looper.getMainLooper()) + .post { + val builder = MaterialAlertDialogBuilder(activity) + + if (args.title != null) { + builder.setTitle(args.title) + } + + val okButtonLabel = args.okButtonLabel ?: "Ok" + + builder + .setMessage(args.message) + .setPositiveButton(okButtonLabel) { dialog, _ -> + dialog.dismiss() + handler(okButtonLabel) + } + .setOnCancelListener { dialog -> + dialog.dismiss() + handler(args.cancelButtonLabel ?: "Cancel") + } + + if (args.noButtonLabel != null) { + builder.setNeutralButton(args.noButtonLabel) { dialog, _ -> + dialog.dismiss() + handler(args.noButtonLabel!!) + } + } + + if (args.cancelButtonLabel != null) { + builder.setNegativeButton( args.cancelButtonLabel) { dialog, _ -> + dialog.dismiss() + handler(args.cancelButtonLabel!!) + } + } + + builder.show() + } + } + + @Command + fun saveFileDialog(invoke: Invoke) { + try { + val args = invoke.parseArgs(SaveFileDialogOptions::class.java) + val parsedTypes = parseFiltersOption(args.filters) + + val intent = Intent(Intent.ACTION_CREATE_DOCUMENT) + intent.addCategory(Intent.CATEGORY_OPENABLE) + intent.putExtra(Intent.EXTRA_TITLE, args.fileName ?: "") + if (parsedTypes.size == 1) { + intent.type = parsedTypes.first() + } else { + intent.type = "*/*" + intent.putExtra(Intent.EXTRA_MIME_TYPES, parsedTypes) + } + + startActivityForResult(invoke, intent, "saveFileDialogResult") + } catch (ex: Exception) { + val message = ex.message ?: "Failed to pick save file" + Logger.error(message) + invoke.reject(message) + } + } + + @ActivityCallback + fun saveFileDialogResult(invoke: Invoke, result: ActivityResult) { + try { + when (result.resultCode) { + Activity.RESULT_OK -> { + val callResult = JSObject() + val intent: Intent? = result.data + if (intent != null) { + val uri = intent.data + if (uri != null) { + callResult.put("file", uri.toString()) + } + } + invoke.resolve(callResult) + } + Activity.RESULT_CANCELED -> invoke.reject("File picker cancelled") + else -> invoke.reject("Failed to pick files") + } + } catch (ex: java.lang.Exception) { + val message = ex.message ?: "Failed to read file pick result" + Logger.error(message) + invoke.reject(message) + } + } +} \ No newline at end of file diff --git a/plugins/dialog/android/src/main/java/FilePickerUtils.kt b/plugins/dialog/android/src/main/java/FilePickerUtils.kt new file mode 100644 index 0000000..7029d4c --- /dev/null +++ b/plugins/dialog/android/src/main/java/FilePickerUtils.kt @@ -0,0 +1,230 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +package app.tauri.dialog + + +import android.content.ContentUris +import android.database.Cursor +import android.provider.MediaStore +import android.content.Context +import android.graphics.BitmapFactory +import android.media.MediaMetadataRetriever +import android.net.Uri +import android.os.Environment +import android.provider.DocumentsContract +import android.provider.OpenableColumns +import android.util.Base64 +import app.tauri.Logger +import java.io.ByteArrayOutputStream +import java.io.FileNotFoundException +import java.io.IOException +import java.io.InputStream + +class FilePickerUtils { + class FileResolution(var height: Int, var width: Int) + + companion object { + fun getPathFromUri(context: Context, uri: Uri): String? { + if (DocumentsContract.isDocumentUri(context, uri)) { + if (isExternalStorageDocument(uri)) { + val docId = DocumentsContract.getDocumentId(uri) + val split = docId.split(":") + return if ("primary".equals(split[0], ignoreCase = true)) { + "${Environment.getExternalStorageDirectory()}/${split[1]}" + } else { + null + } + } else if (isDownloadsDocument(uri)) { + val id = DocumentsContract.getDocumentId(uri) + val contentUri = ContentUris.withAppendedId( + Uri.parse("content://downloads/public_downloads"), java.lang.Long.valueOf(id)) + return getDataColumn(context, contentUri, null, null) + } else if (isMediaDocument(uri)) { + val docId = DocumentsContract.getDocumentId(uri) + val split = docId.split(":") + val contentUri: Uri? = when (split[0]) { + "image" -> MediaStore.Images.Media.EXTERNAL_CONTENT_URI + "video" -> MediaStore.Video.Media.EXTERNAL_CONTENT_URI + "audio" -> MediaStore.Audio.Media.EXTERNAL_CONTENT_URI + else -> null + } + val selection = "_id=?" + val selectionArgs = arrayOf(split[1]) + return getDataColumn(context, contentUri, selection, selectionArgs) + } + } else if ("content".equals(uri.scheme, ignoreCase = true)) { + return getDataColumn(context, uri, null, null) + } else if ("file".equals(uri.scheme, ignoreCase = true)) { + return uri.path + } + return null + } + + fun getNameFromUri(context: Context, uri: Uri): String? { + var displayName: String? = "" + val projection = arrayOf(OpenableColumns.DISPLAY_NAME) + val cursor = + context.contentResolver.query(uri, projection, null, null, null) + if (cursor != null) { + cursor.moveToFirst() + val columnIdx = cursor.getColumnIndex(projection[0]) + displayName = cursor.getString(columnIdx) + cursor.close() + } + if (displayName.isNullOrEmpty()) { + displayName = uri.lastPathSegment + } + return displayName + } + + fun getDataFromUri(context: Context, uri: Uri): String { + try { + val stream = context.contentResolver.openInputStream(uri) ?: return "" + val bytes = getBytesFromInputStream(stream) + return Base64.encodeToString(bytes, Base64.NO_WRAP) + } catch (e: FileNotFoundException) { + Logger.error("openInputStream failed.", e) + } catch (e: IOException) { + Logger.error("getBytesFromInputStream failed.", e) + } + return "" + } + + fun getMimeTypeFromUri(context: Context, uri: Uri): String? { + return context.contentResolver.getType(uri) + } + + fun getModifiedAtFromUri(context: Context, uri: Uri): Long? { + return try { + var modifiedAt: Long = 0 + val cursor = + context.contentResolver.query(uri, null, null, null, null) + if (cursor != null) { + cursor.moveToFirst() + val columnIdx = + cursor.getColumnIndex(DocumentsContract.Document.COLUMN_LAST_MODIFIED) + modifiedAt = cursor.getLong(columnIdx) + cursor.close() + } + modifiedAt + } catch (e: Exception) { + Logger.error("getModifiedAtFromUri failed.", e) + null + } + } + + fun getSizeFromUri(context: Context, uri: Uri): Long { + var size: Long = 0 + val projection = arrayOf(OpenableColumns.SIZE) + val cursor = + context.contentResolver.query(uri, projection, null, null, null) + if (cursor != null) { + cursor.moveToFirst() + val columnIdx = cursor.getColumnIndex(projection[0]) + size = cursor.getLong(columnIdx) + cursor.close() + } + return size + } + + fun getDurationFromUri(context: Context, uri: Uri): Long? { + if (isVideoUri(context, uri)) { + val retriever = MediaMetadataRetriever() + retriever.setDataSource(context, uri) + val time = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION) + val durationMs = time?.toLong() ?: 0 + try { + retriever.release() + } catch (e: Exception) { + Logger.error("MediaMetadataRetriever.release() failed.", e) + } + return durationMs / 1000L + } + return null + } + + fun getHeightAndWidthFromUri(context: Context, uri: Uri): FileResolution? { + if (isImageUri(context, uri)) { + val options = BitmapFactory.Options() + options.inJustDecodeBounds = true + return try { + BitmapFactory.decodeStream( + context.contentResolver.openInputStream(uri), + null, + options + ) + FileResolution(options.outHeight, options.outWidth) + } catch (exception: FileNotFoundException) { + exception.printStackTrace() + null + } + } else if (isVideoUri(context, uri)) { + val retriever = MediaMetadataRetriever() + retriever.setDataSource(context, uri) + val width = + Integer.valueOf(retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_WIDTH) ?: "0") + val height = + Integer.valueOf(retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_HEIGHT) ?: "0") + try { + retriever.release() + } catch (e: Exception) { + Logger.error("MediaMetadataRetriever.release() failed.", e) + } + return FileResolution(height, width) + } + return null + } + + private fun isImageUri(context: Context, uri: Uri): Boolean { + val mimeType = getMimeTypeFromUri(context, uri) ?: return false + return mimeType.startsWith("image") + } + + private fun isVideoUri(context: Context, uri: Uri): Boolean { + val mimeType = getMimeTypeFromUri(context, uri) ?: return false + return mimeType.startsWith("video") + } + + @Throws(IOException::class) + private fun getBytesFromInputStream(`is`: InputStream): ByteArray { + val os = ByteArrayOutputStream() + val buffer = ByteArray(0xFFFF) + var len = `is`.read(buffer) + while (len != -1) { + os.write(buffer, 0, len) + len = `is`.read(buffer) + } + return os.toByteArray() + } + } +} + +private fun getDataColumn(context: Context, uri: Uri?, selection: String?, selectionArgs: Array?): String? { + var cursor: Cursor? = null + val column = "_data" + val projection = arrayOf(column) + try { + cursor = context.contentResolver.query(uri!!, projection, selection, selectionArgs, null) + if (cursor != null && cursor.moveToFirst()) { + val columnIndex = cursor.getColumnIndexOrThrow(column) + return cursor.getString(columnIndex) + } + } finally { + cursor?.close() + } + return null +} + +private fun isExternalStorageDocument(uri: Uri): Boolean { + return "com.android.externalstorage.documents" == uri.authority +} + +private fun isDownloadsDocument(uri: Uri): Boolean { + return "com.android.providers.downloads.documents" == uri.authority +} + +private fun isMediaDocument(uri: Uri): Boolean { + return "com.android.providers.media.documents" == uri.authority +} diff --git a/plugins/dialog/android/src/test/java/ExampleUnitTest.kt b/plugins/dialog/android/src/test/java/ExampleUnitTest.kt new file mode 100644 index 0000000..00369a3 --- /dev/null +++ b/plugins/dialog/android/src/test/java/ExampleUnitTest.kt @@ -0,0 +1,21 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +package app.tauri.dialog + +import org.junit.Test + +import org.junit.Assert.* + +/** + * Example local unit test, which will execute on the development machine (host). + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +class ExampleUnitTest { + @Test + fun addition_isCorrect() { + assertEquals(4, 2 + 2) + } +} diff --git a/plugins/dialog/api-iife.js b/plugins/dialog/api-iife.js new file mode 100644 index 0000000..8c11c21 --- /dev/null +++ b/plugins/dialog/api-iife.js @@ -0,0 +1 @@ +if("__TAURI__"in window){var __TAURI_PLUGIN_DIALOG__=function(n,e){"use strict";function t(n){if(void 0!==n)return"string"==typeof n?n:"ok"in n&&"cancel"in n?{OkCancelCustom:[n.ok,n.cancel]}:"yes"in n&&"no"in n&&"cancel"in n?{YesNoCancelCustom:[n.yes,n.no,n.cancel]}:"ok"in n?{OkCustom:n.ok}:void 0}async function o(n,o){return await e.invoke("plugin:dialog|message",{message:n,title:o?.title,kind:o?.kind,buttons:t(o?.buttons)})}return n.ask=async function(n,e){const t="string"==typeof e?{title:e}:e,i=t?.okLabel||t?.cancelLabel,a=t?.okLabel??"Yes";return await o(n,{title:t?.title,kind:t?.kind,buttons:i?{ok:a,cancel:t.cancelLabel??"No"}:"YesNo"})===a},n.confirm=async function(n,e){const t="string"==typeof e?{title:e}:e,i=t?.okLabel||t?.cancelLabel,a=t?.okLabel??"Ok";return await o(n,{title:t?.title,kind:t?.kind,buttons:i?{ok:a,cancel:t.cancelLabel??"Cancel"}:"OkCancel"})===a},n.message=async function(n,e){const t="string"==typeof e?{title:e}:e;return t&&!t.buttons&&t.okLabel&&(t.buttons={ok:t.okLabel}),o(n,t)},n.open=async function(n={}){return"object"==typeof n&&Object.freeze(n),await e.invoke("plugin:dialog|open",{options:n})},n.save=async function(n={}){return"object"==typeof n&&Object.freeze(n),await e.invoke("plugin:dialog|save",{options:n})},n}({},window.__TAURI__.core);Object.defineProperty(window.__TAURI__,"dialog",{value:__TAURI_PLUGIN_DIALOG__})} diff --git a/plugins/dialog/banner.png b/plugins/dialog/banner.png new file mode 100644 index 0000000000000000000000000000000000000000..88be34e452511ed71963ae42275704a62a8c8ac6 GIT binary patch literal 57224 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

qcSg~MzLbub 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 literal 0 HcmV?d00001 diff --git a/plugins/dialog/build.rs b/plugins/dialog/build.rs new file mode 100644 index 0000000..a94f61a --- /dev/null +++ b/plugins/dialog/build.rs @@ -0,0 +1,18 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +const COMMANDS: &[&str] = &["open", "save", "message"]; + +fn main() { + let result = tauri_plugin::Builder::new(COMMANDS) + .global_api_script_path("./api-iife.js") + .android_path("android") + .ios_path("ios") + .try_build(); + + // when building documentation for Android the plugin build result is always Err() and is irrelevant to the crate documentation build + if !(cfg!(docsrs) && std::env::var("TARGET").unwrap().contains("android")) { + result.unwrap(); + } +} diff --git a/plugins/dialog/guest-js/index.ts b/plugins/dialog/guest-js/index.ts new file mode 100644 index 0000000..2ca5781 --- /dev/null +++ b/plugins/dialog/guest-js/index.ts @@ -0,0 +1,554 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +/** + * Native system dialogs for opening and saving files, along with message, ask and confirm dialogs. + * + * @module + */ + +import { invoke } from '@tauri-apps/api/core' + +/** + * Extension filters for the file dialog. + * + * @since 2.0.0 + */ +interface DialogFilter { + /** The name of the filter, shown to the user in the dialog's filter selector. */ + name: string + /** + * Extensions to filter, without a `.` prefix. + * + * **Note:** Mobile platforms have different APIs for filtering that may not support extensions. + * iOS: Extensions are supported in the document picker, but not in the media picker. + * Android: Extensions are not supported. + * + * For these platforms, MIME types are the primary way to filter files, as opposed to extensions. + * This means the string values here labeled as `extensions` may also be a MIME type. + * This property name of `extensions` is being kept for backwards compatibility, but this may be revisited to + * specify the difference between extension or MIME type filtering. + * + * @example + * ```typescript + * extensions: ['svg', 'png'] + * ``` + */ + extensions: string[] +} + +/** + * Options for the open dialog. + * + * @since 2.0.0 + */ +interface OpenDialogOptions { + /** The title of the dialog window (desktop only). */ + title?: string + /** + * The filters of the dialog. + * On mobile platforms, if either: + * A) the {@linkcode pickerMode} is set to `media`, `image`, or `video` + * -- or -- + * B) the filters include **only** either image or video mime types, the media picker will be displayed. + * Otherwise, the document picker will be displayed. + */ + filters?: DialogFilter[] + /** + * Initial directory or file path. + * If it's a directory path, the dialog interface will change to that folder. + * If it's not an existing directory, the file name will be set to the dialog's file name input and the dialog will be set to the parent folder. + * + * On mobile the file name is always used on the dialog's file name input. + * If not provided, Android uses `(invalid).txt` as default file name. + */ + defaultPath?: string + /** Whether the dialog allows multiple selection or not. */ + multiple?: boolean + /** Whether the dialog is a directory selection or not. */ + directory?: boolean + /** + * If `directory` is true, indicates that it will be read recursively later. + * Defines whether subdirectories will be allowed on the scope or not. + */ + recursive?: boolean + /** Whether to allow creating directories in the dialog. Enabled by default. **macOS Only** */ + canCreateDirectories?: boolean + /** + * The preferred mode of the dialog. + * This is meant for mobile platforms (iOS and Android) which have distinct file and media pickers. + * If not provided, the dialog will automatically choose the best mode based on the MIME types or extensions of the {@linkcode filters}. + * On desktop, this option is ignored. + */ + pickerMode?: PickerMode + /** + * The file access mode of the dialog. + * If not provided, `copy` is used, which matches the behavior of the {@linkcode open} method before the introduction of this option. + * + * **Usage** + * If a file is opened with {@linkcode fileAccessMode: 'copy'}, it will be copied to the app's sandbox. + * This means the file can be read, edited, deleted, copied, or any other operation without any issues, since the file + * now belongs to the app. + * This also means that the caller has responsibility of deleting the file if this file is not meant to be retained + * in the app sandbox. + * + * If a file is opened with {@linkcode fileAccessMode: 'scoped'}, the file will remain in its original location + * and security-scoped access will be automatically managed by the system. + * + * **Note** + * This is specifically meant for document pickers on iOS or MacOS, in conjunction with [security scoped resources](https://developer.apple.com/documentation/foundation/nsurl/startaccessingsecurityscopedresource()). + * + * Why only document pickers, and not image or video pickers? + * The image and video pickers on iOS behave differently from the document pickers, and return [NSItemProvider](https://developer.apple.com/documentation/foundation/nsitemprovider) objects instead of file URLs. + * These are meant to be ephemeral (only available within the callback of the picker), and are not accessible outside of the callback. + * So for image and video pickers, the only way to access the file is to copy it to the app's sandbox, and this is the URL that is returned from this API. + * This means there is no provision for using `scoped` mode with image or video pickers. + * If an image or video picker is used, `copy` is always used. + */ + fileAccessMode?: FileAccessMode +} + +/** + * Options for the save dialog. + * + * @since 2.0.0 + */ +interface SaveDialogOptions { + /** The title of the dialog window (desktop only). */ + title?: string + /** The filters of the dialog. */ + filters?: DialogFilter[] + /** + * Initial directory or file path. + * If it's a directory path, the dialog interface will change to that folder. + * If it's not an existing directory, the file name will be set to the dialog's file name input and the dialog will be set to the parent folder. + * + * On mobile the file name is always used on the dialog's file name input. + * If not provided, Android uses `(invalid).txt` as default file name. + */ + defaultPath?: string + /** Whether to allow creating directories in the dialog. Enabled by default. **macOS Only** */ + canCreateDirectories?: boolean +} + +/** + * The preferred mode of the dialog. + * This is meant for mobile platforms (iOS and Android) which have distinct file and media pickers. + * On desktop, this option is ignored. + * If not provided, the dialog will automatically choose the best mode based on the MIME types or extensions of the {@linkcode filters}. + * + * **Note:** This option is only supported on iOS 14 and above. This parameter is ignored on iOS 13 and below. + */ +export type PickerMode = 'document' | 'media' | 'image' | 'video' + +/** + * The file access mode of the dialog. + * + * - `copy`: copy/move the picked file to the app sandbox; no scoped access required. + * - `scoped`: keep file in place; security-scoped access is automatically managed. + * + * **Note:** This option is only supported on iOS 14 and above. This parameter is ignored on iOS 13 and below. + */ +export type FileAccessMode = 'copy' | 'scoped' + +/** + * Default buttons for a message dialog. + * + * @since 2.4.0 + */ +export type MessageDialogDefaultButtons = + | 'Ok' + | 'OkCancel' + | 'YesNo' + | 'YesNoCancel' + +/** All possible button keys. */ +type ButtonKey = 'ok' | 'cancel' | 'yes' | 'no' + +/** Ban everything except a set of keys. */ +type BanExcept = Partial< + Record, never> +> + +/** + * The Yes, No and Cancel buttons of a message dialog. + * + * @since 2.4.0 + */ +export type MessageDialogButtonsYesNoCancel = { + /** The label of the Yes button. */ + yes: string + /** The label of the No button. */ + no: string + /** The label of the Cancel button. */ + cancel: string +} & BanExcept<'yes' | 'no' | 'cancel'> + +/** + * The Ok and Cancel buttons of a message dialog. + * + * @since 2.4.0 + */ +export type MessageDialogButtonsOkCancel = { + /** The label of the Ok button. */ + ok: string + /** The label of the Cancel button. */ + cancel: string +} & BanExcept<'ok' | 'cancel'> + +/** + * The Ok button of a message dialog. + * + * @since 2.4.0 + */ +export type MessageDialogButtonsOk = { + /** The label of the Ok button. */ + ok: string +} & BanExcept<'ok'> + +/** + * Custom buttons for a message dialog. + * + * @since 2.4.0 + */ +export type MessageDialogCustomButtons = + | MessageDialogButtonsYesNoCancel + | MessageDialogButtonsOkCancel + | MessageDialogButtonsOk + +/** + * The buttons of a message dialog. + * + * @since 2.4.0 + */ +export type MessageDialogButtons = + | MessageDialogDefaultButtons + | MessageDialogCustomButtons + +/** + * Options for the message dialog. + * + * @since 2.0.0 + */ +interface MessageDialogOptions { + /** The title of the dialog. Defaults to the app name. */ + title?: string + /** The kind of the dialog. Defaults to `info`. */ + kind?: 'info' | 'warning' | 'error' + /** + * The label of the Ok button. + * + * @deprecated Use {@linkcode MessageDialogOptions.buttons} instead. + */ + okLabel?: string + /** + * The buttons of the dialog. + * + * @example + * + * ```ts + * // Use system default buttons texts + * await message('Hello World!', { buttons: 'Ok' }) + * await message('Hello World!', { buttons: 'OkCancel' }) + * + * // Or with custom button texts + * await message('Hello World!', { buttons: { ok: 'Yes!' } }) + * await message('Take on the task?', { + * buttons: { ok: 'Accept', cancel: 'Cancel' } + * }) + * await message('Show the file content?', { + * buttons: { yes: 'Show content', no: 'Show in folder', cancel: 'Cancel' } + * }) + * ``` + * + * @since 2.4.0 + */ + buttons?: MessageDialogButtons +} + +/** + * Internal function to convert the buttons to the Rust type. + */ +function buttonsToRust(buttons: MessageDialogButtons | undefined) { + if (buttons === undefined) { + return undefined + } + + if (typeof buttons === 'string') { + return buttons + } else if ('ok' in buttons && 'cancel' in buttons) { + return { OkCancelCustom: [buttons.ok, buttons.cancel] } + } else if ('yes' in buttons && 'no' in buttons && 'cancel' in buttons) { + return { + YesNoCancelCustom: [buttons.yes, buttons.no, buttons.cancel] + } + } else if ('ok' in buttons) { + return { OkCustom: buttons.ok } + } + + return undefined +} + +/** + * Options for the {@linkcode ask} and {@linkcode confirm} dialogs. + * + * @since 2.0.0 + */ +interface ConfirmDialogOptions { + /** The title of the dialog. Defaults to the app name. */ + title?: string + /** The kind of the dialog. Defaults to `info`. */ + kind?: 'info' | 'warning' | 'error' + /** The label of the confirm button. */ + okLabel?: string + /** The label of the cancel button. */ + cancelLabel?: string +} + +/** + * The return type of {@linkcode open}, derived from its options: a single path, an array of + * paths when {@linkcode OpenDialogOptions.multiple} is `true`, or `null` when the user cancels. + * + * @since 2.0.0 + */ +type OpenDialogReturn = T['directory'] extends true + ? T['multiple'] extends true + ? string[] | null + : string | null + : T['multiple'] extends true + ? string[] | null + : string | null + +/** + * Open a file/directory selection dialog. + * + * The selected paths are added to the filesystem and asset protocol scopes. + * When security is more important than the easy of use of this API, + * prefer writing a dedicated command instead. + * + * Note that the scope change is not persisted, so the values are cleared when the application is restarted. + * You can save it to the filesystem using [tauri-plugin-persisted-scope](https://github.com/tauri-apps/tauri-plugin-persisted-scope). + * @example + * ```typescript + * import { open } from '@tauri-apps/plugin-dialog'; + * // Open a selection dialog for image files + * const selected = await open({ + * multiple: true, + * filters: [{ + * name: 'Image', + * extensions: ['png', 'jpeg'] + * }] + * }); + * if (Array.isArray(selected)) { + * // user selected multiple files + * } else if (selected === null) { + * // user cancelled the selection + * } else { + * // user selected a single file + * } + * ``` + * + * @example + * ```typescript + * import { open } from '@tauri-apps/plugin-dialog'; + * import { appDir } from '@tauri-apps/api/path'; + * // Open a selection dialog for directories + * const selected = await open({ + * directory: true, + * multiple: true, + * defaultPath: await appDir(), + * }); + * if (Array.isArray(selected)) { + * // user selected multiple directories + * } else if (selected === null) { + * // user cancelled the selection + * } else { + * // user selected a single directory + * } + * ``` + * + * @param options The dialog's options. + * + * @returns A promise resolving to the selected path(s) + * + * @since 2.0.0 + */ +async function open( + options: T = {} as T +): Promise> { + if (typeof options === 'object') { + Object.freeze(options) + } + + return await invoke('plugin:dialog|open', { options }) +} + +/** + * Open a file/directory save dialog. + * + * The selected path is added to the filesystem and asset protocol scopes. + * When security is more important than the easy of use of this API, + * prefer writing a dedicated command instead. + * + * Note that the scope change is not persisted, so the values are cleared when the application is restarted. + * You can save it to the filesystem using [tauri-plugin-persisted-scope](https://github.com/tauri-apps/tauri-plugin-persisted-scope). + * @example + * ```typescript + * import { save } from '@tauri-apps/plugin-dialog'; + * const filePath = await save({ + * filters: [{ + * name: 'Image', + * extensions: ['png', 'jpeg'] + * }] + * }); + * ``` + * + * @param options The dialog's options. + * + * @returns A promise resolving to the selected path. + * + * @since 2.0.0 + */ +async function save(options: SaveDialogOptions = {}): Promise { + if (typeof options === 'object') { + Object.freeze(options) + } + + return await invoke('plugin:dialog|save', { options }) +} + +/** + * The result of a message dialog. + * + * The result is a string if the dialog has custom buttons, + * otherwise it is one of the default buttons. + * + * @since 2.4.0 + */ +export type MessageDialogResult = 'Yes' | 'No' | 'Ok' | 'Cancel' | (string & {}) + +async function messageCommand( + message: string, + options?: Omit +) { + return await invoke('plugin:dialog|message', { + message, + title: options?.title, + kind: options?.kind, + buttons: buttonsToRust(options?.buttons) + }) +} + +/** + * Shows a message dialog with an `Ok` button. + * @example + * ```typescript + * import { message } from '@tauri-apps/plugin-dialog'; + * await message('Tauri is awesome', 'Tauri'); + * await message('File not found', { title: 'Tauri', kind: 'error' }); + * ``` + * + * @param message The message to show. + * @param options The dialog's options. If a string, it represents the dialog title. + * + * @returns A promise indicating the success or failure of the operation. + * + * @since 2.0.0 + * + */ +async function message( + message: string, + options?: string | MessageDialogOptions +): Promise { + const opts = typeof options === 'string' ? { title: options } : options + if (opts && !opts.buttons && opts.okLabel) { + opts.buttons = { ok: opts.okLabel } + } + return messageCommand(message, opts) +} + +/** + * Shows a question dialog with `Yes` and `No` buttons. + * + * Convenient wrapper for `await message('msg', { buttons: 'YesNo' }) === 'Yes'` + * + * @example + * ```typescript + * import { ask } from '@tauri-apps/plugin-dialog'; + * const yes = await ask('Are you sure?', 'Tauri'); + * const yes2 = await ask('This action cannot be reverted. Are you sure?', { title: 'Tauri', kind: 'warning' }); + * ``` + * + * @param message The message to show. + * @param options The dialog's options. If a string, it represents the dialog title. + * + * @returns A promise resolving to a boolean indicating whether `Yes` was clicked or not. + * + * @since 2.0.0 + */ +async function ask( + message: string, + options?: string | ConfirmDialogOptions +): Promise { + const opts = typeof options === 'string' ? { title: options } : options + const customButtons = opts?.okLabel || opts?.cancelLabel + const okLabel = opts?.okLabel ?? 'Yes' + return ( + (await messageCommand(message, { + title: opts?.title, + kind: opts?.kind, + buttons: customButtons + ? { ok: okLabel, cancel: opts.cancelLabel ?? 'No' } + : 'YesNo' + })) === okLabel + ) +} + +/** + * Shows a question dialog with `Ok` and `Cancel` buttons. + * + * Convenient wrapper for `await message('msg', { buttons: 'OkCancel' }) === 'Ok'` + * + * @example + * ```typescript + * import { confirm } from '@tauri-apps/plugin-dialog'; + * const confirmed = await confirm('Are you sure?', 'Tauri'); + * const confirmed2 = await confirm('This action cannot be reverted. Are you sure?', { title: 'Tauri', kind: 'warning' }); + * ``` + * + * @param message The message to show. + * @param options The dialog's options. If a string, it represents the dialog title. + * + * @returns A promise resolving to a boolean indicating whether `Ok` was clicked or not. + * + * @since 2.0.0 + */ +async function confirm( + message: string, + options?: string | ConfirmDialogOptions +): Promise { + const opts = typeof options === 'string' ? { title: options } : options + const customButtons = opts?.okLabel || opts?.cancelLabel + const okLabel = opts?.okLabel ?? 'Ok' + return ( + (await messageCommand(message, { + title: opts?.title, + kind: opts?.kind, + buttons: customButtons + ? { ok: okLabel, cancel: opts.cancelLabel ?? 'Cancel' } + : 'OkCancel' + })) === okLabel + ) +} + +export type { + DialogFilter, + OpenDialogOptions, + OpenDialogReturn, + SaveDialogOptions, + MessageDialogOptions, + ConfirmDialogOptions +} + +export { open, save, message, ask, confirm } diff --git a/plugins/dialog/guest-js/init.ts b/plugins/dialog/guest-js/init.ts new file mode 100644 index 0000000..f272c1b --- /dev/null +++ b/plugins/dialog/guest-js/init.ts @@ -0,0 +1,25 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +/** + * Injected script that replaces `window.alert` and `window.confirm` with implementations backed + * by native dialogs. + * + * @module + */ + +import { invoke } from '@tauri-apps/api/core' + +window.alert = function (message: string) { + void invoke('plugin:dialog|message', { + message: message.toString() + }) +} + +// @ts-expect-error tauri does not have sync IPC :( +window.confirm = async function (message: string) { + return await invoke('plugin:dialog|confirm', { + message: message.toString() + }) +} diff --git a/plugins/dialog/ios/.gitignore b/plugins/dialog/ios/.gitignore new file mode 100644 index 0000000..5922fda --- /dev/null +++ b/plugins/dialog/ios/.gitignore @@ -0,0 +1,10 @@ +.DS_Store +/.build +/Packages +/*.xcodeproj +xcuserdata/ +DerivedData/ +.swiftpm/config/registries.json +.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata +.netrc +Package.resolved diff --git a/plugins/dialog/ios/Package.swift b/plugins/dialog/ios/Package.swift new file mode 100644 index 0000000..f8983f1 --- /dev/null +++ b/plugins/dialog/ios/Package.swift @@ -0,0 +1,34 @@ +// swift-tools-version:5.3 +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +import PackageDescription + +let package = Package( + name: "tauri-plugin-dialog", + platforms: [ + .macOS(.v10_13), + .iOS(.v13), + ], + products: [ + // Products define the executables and libraries a package produces, and make them visible to other packages. + .library( + name: "tauri-plugin-dialog", + type: .static, + targets: ["tauri-plugin-dialog"]) + ], + dependencies: [ + .package(name: "Tauri", path: "../.tauri/tauri-api") + ], + targets: [ + // Targets are the basic building blocks of a package. A target can define a module or a test suite. + // Targets can depend on other targets in this package, and on products in packages this package depends on. + .target( + name: "tauri-plugin-dialog", + dependencies: [ + .byName(name: "Tauri") + ], + path: "Sources") + ] +) diff --git a/plugins/dialog/ios/README.md b/plugins/dialog/ios/README.md new file mode 100644 index 0000000..157cf03 --- /dev/null +++ b/plugins/dialog/ios/README.md @@ -0,0 +1,3 @@ +# Tauri Plugin Dialog + +A description of this package. diff --git a/plugins/dialog/ios/Sources/DialogPlugin.swift b/plugins/dialog/ios/Sources/DialogPlugin.swift new file mode 100644 index 0000000..719c345 --- /dev/null +++ b/plugins/dialog/ios/Sources/DialogPlugin.swift @@ -0,0 +1,313 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +import MobileCoreServices +import Photos +import PhotosUI +import SwiftRs +import Tauri +import UIKit +import UniformTypeIdentifiers +import WebKit + +enum FilePickerEvent { + case selected([URL]) + case cancelled + case error(String) +} + +struct MessageDialogOptions: Decodable { + var title: String? + let message: String + var okButtonLabel: String? + var noButtonLabel: String? + var cancelButtonLabel: String? +} + +struct Filter: Decodable { + var extensions: [String]? +} + +struct FilePickerOptions: Decodable { + var multiple: Bool? + var filters: [Filter]? + var defaultPath: String? + var pickerMode: PickerMode? + var fileAccessMode: FileAccessMode? +} + +struct SaveFileDialogOptions: Decodable { + var fileName: String? + var defaultPath: String? +} + +enum FileAccessMode: String, Decodable { + case copy + case scoped +} + +enum PickerMode: String, Decodable { + case document + case media + case image + case video +} + +class DialogPlugin: Plugin { + + var filePickerController: FilePickerController! + var onFilePickerResult: ((FilePickerEvent) -> Void)? = nil + + override init() { + super.init() + filePickerController = FilePickerController(self) + + } + + @objc public func showFilePicker(_ invoke: Invoke) throws { + let args = try invoke.parseArgs(FilePickerOptions.self) + + onFilePickerResult = { (event: FilePickerEvent) -> Void in + switch event { + case .selected(let urls): + invoke.resolve(["files": urls]) + case .cancelled: + invoke.resolve(["files": nil]) + case .error(let error): + invoke.reject(error) + } + } + + if #available(iOS 14, *) { + let parsedTypes = parseFiltersOption(args.filters ?? []) + + let mimeKinds = Set( + parsedTypes.compactMap { $0.preferredMIMEType?.components(separatedBy: "/")[0] }) + let filtersIncludeImage = mimeKinds.contains("image") + let filtersIncludeVideo = mimeKinds.contains("video") + let filtersIncludeNonMedia = mimeKinds.contains(where: { $0 != "image" && $0 != "video" }) + + // If the picker mode is media, images, or videos, we always want to show the media picker regardless of what's in the filters. + // Otherwise, if the filters A) do not include non-media types and B) include either image or video, we want to show the media picker. + if args.pickerMode == .media + || args.pickerMode == .image + || args.pickerMode == .video + || (!filtersIncludeNonMedia && (filtersIncludeImage || filtersIncludeVideo)) + { + DispatchQueue.main.async { + var configuration = PHPickerConfiguration(photoLibrary: PHPhotoLibrary.shared()) + configuration.selectionLimit = (args.multiple ?? false) ? 0 : 1 + + // If the filters include image or video, use the appropriate filter. + // If both are true, don't define a filter, which means we will display all media. + if args.pickerMode == .image || (filtersIncludeImage && !filtersIncludeVideo) { + configuration.filter = .images + } else if args.pickerMode == .video || (filtersIncludeVideo && !filtersIncludeImage) { + configuration.filter = .videos + } + + let picker = PHPickerViewController(configuration: configuration) + picker.delegate = self.filePickerController + picker.modalPresentationStyle = .fullScreen + self.presentViewController(picker) + } + } else { + DispatchQueue.main.async { + // The UTType.item is the catch-all, allowing for any file type to be selected. + let contentTypes = parsedTypes.isEmpty ? [UTType.item] : parsedTypes + let picker: UIDocumentPickerViewController = UIDocumentPickerViewController( + forOpeningContentTypes: contentTypes, + asCopy: args.fileAccessMode == .scoped ? false : true) + + if let defaultPath = args.defaultPath { + picker.directoryURL = URL(string: defaultPath) + } + + picker.delegate = self.filePickerController + picker.allowsMultipleSelection = args.multiple ?? false + picker.modalPresentationStyle = .fullScreen + self.presentViewController(picker) + } + } + } else { + showFilePickerLegacy(args: args) + } + } + + @objc public func saveFileDialog(_ invoke: Invoke) throws { + let args = try invoke.parseArgs(SaveFileDialogOptions.self) + + // The Tauri save dialog API prompts the user to select a path where a file must be saved + // This behavior maps to the operating system interfaces on all platforms except iOS, + // which only exposes a mechanism to "move file `srcPath` to a location defined by the user" + // + // so we have to work around it by creating an empty file matching the requested `args.fileName`, + // and using it as `srcPath` for the operation - returning the path the user selected + // so the app dev can write to it later - matching cross platform behavior as mentioned above + let fileManager = FileManager.default + let srcFolder = fileManager.urls(for: .documentDirectory, in: .userDomainMask).first! + let srcPath = srcFolder.appendingPathComponent(args.fileName ?? "file") + if !fileManager.fileExists(atPath: srcPath.path) { + // the file contents must be actually provided by the tauri dev after the path is resolved by the save API + try "".write(to: srcPath, atomically: true, encoding: .utf8) + } + + onFilePickerResult = { (event: FilePickerEvent) -> Void in + switch event { + case .selected(let urls): + invoke.resolve(["file": urls.first!]) + case .cancelled: + invoke.resolve(["file": nil]) + case .error(let error): + invoke.reject(error) + } + } + + DispatchQueue.main.async { + let picker = UIDocumentPickerViewController(url: srcPath, in: .exportToService) + if let defaultPath = args.defaultPath { + picker.directoryURL = URL(string: defaultPath) + } + picker.delegate = self.filePickerController + picker.modalPresentationStyle = .fullScreen + self.presentViewController(picker) + } + } + + private func presentViewController(_ viewControllerToPresent: UIViewController) { + self.manager.viewController?.present(viewControllerToPresent, animated: true, completion: nil) + } + + @available(iOS 14, *) + private func parseFiltersOption(_ filters: [Filter]) -> [UTType] { + var parsedTypes: [UTType] = [] + for filter in filters { + for ext in filter.extensions ?? [] { + // We need to support extensions as well as MIME types. + if let utType = UTType(mimeType: ext) { + parsedTypes.append(utType) + } else if let utType = UTType(filenameExtension: ext) { + parsedTypes.append(utType) + } + } + } + + return parsedTypes + } + + /// This function is only used for iOS < 14, and should be removed if/when the deployment target is raised to 14. + private func showFilePickerLegacy(args: FilePickerOptions) { + let parsedTypes = parseFiltersOptionLegacy(args.filters ?? []) + + var filtersIncludeImage: Bool = false + var filtersIncludeVideo: Bool = false + var filtersIncludeNonMedia: Bool = false + + if !parsedTypes.isEmpty { + let mimeKinds = Set(parsedTypes.map { $0.components(separatedBy: "/")[0] }) + filtersIncludeImage = mimeKinds.contains("image") + filtersIncludeVideo = mimeKinds.contains("video") + filtersIncludeNonMedia = mimeKinds.contains(where: { $0 != "image" && $0 != "video" }) + } + + if !filtersIncludeNonMedia && (filtersIncludeImage || filtersIncludeVideo) { + DispatchQueue.main.async { + let picker = UIImagePickerController() + picker.delegate = self.filePickerController + + if filtersIncludeImage && !filtersIncludeVideo { + picker.sourceType = .photoLibrary + } + + picker.modalPresentationStyle = .fullScreen + self.presentViewController(picker) + } + } else { + let documentTypes = parsedTypes.isEmpty ? ["public.data"] : parsedTypes + DispatchQueue.main.async { + let picker = UIDocumentPickerViewController(documentTypes: documentTypes, in: .import) + if let defaultPath = args.defaultPath { + picker.directoryURL = URL(string: defaultPath) + } + + picker.delegate = self.filePickerController + picker.allowsMultipleSelection = args.multiple ?? false + picker.modalPresentationStyle = .fullScreen + self.presentViewController(picker) + } + } + } + + /// This function is only used for iOS < 14, and should be removed if/when the deployment target is raised to 14. + private func parseFiltersOptionLegacy(_ filters: [Filter]) -> [String] { + var parsedTypes: [String] = [] + for filter in filters { + for ext in filter.extensions ?? [] { + guard + let utType: String = UTTypeCreatePreferredIdentifierForTag( + kUTTagClassMIMEType, ext as CFString, nil)?.takeRetainedValue() as String? + else { + continue + } + parsedTypes.append(utType) + } + } + + return parsedTypes + } + + public func onFilePickerEvent(_ event: FilePickerEvent) { + self.onFilePickerResult?(event) + } + + @objc public func showMessageDialog(_ invoke: Invoke) throws { + let manager = self.manager + let args = try invoke.parseArgs(MessageDialogOptions.self) + + DispatchQueue.main.async { [] in + let alert = UIAlertController( + title: args.title, message: args.message, preferredStyle: UIAlertController.Style.alert) + + if let cancelButtonLabel = args.cancelButtonLabel { + alert.addAction( + UIAlertAction( + title: cancelButtonLabel, style: UIAlertAction.Style.default, + handler: { (_) -> Void in + invoke.resolve(["value": cancelButtonLabel]) + } + ) + ) + } + + if let noButtonLabel = args.noButtonLabel { + alert.addAction( + UIAlertAction( + title: noButtonLabel, style: UIAlertAction.Style.default, + handler: { (_) -> Void in + invoke.resolve(["value": noButtonLabel]) + } + ) + ) + } + + let okButtonLabel = args.okButtonLabel ?? "Ok" + alert.addAction( + UIAlertAction( + title: okButtonLabel, style: UIAlertAction.Style.default, + handler: { (_) -> Void in + invoke.resolve(["value": okButtonLabel]) + } + ) + ) + + manager.viewController?.present(alert, animated: true, completion: nil) + } + } + +} + +@_cdecl("init_plugin_dialog") +func initPlugin() -> Plugin { + return DialogPlugin() +} diff --git a/plugins/dialog/ios/Sources/FilePickerController.swift b/plugins/dialog/ios/Sources/FilePickerController.swift new file mode 100644 index 0000000..99cbb4b --- /dev/null +++ b/plugins/dialog/ios/Sources/FilePickerController.swift @@ -0,0 +1,253 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +import UIKit +import MobileCoreServices +import PhotosUI +import Photos +import Tauri + +public class FilePickerController: NSObject { + var plugin: DialogPlugin + + init(_ dialogPlugin: DialogPlugin) { + plugin = dialogPlugin + } + + private func dismissViewController(_ viewControllerToPresent: UIViewController, completion: (() -> Void)? = nil) { + viewControllerToPresent.dismiss(animated: true, completion: completion) + } + + public func getModifiedAtFromUrl(_ url: URL) -> Int? { + do { + let attributes = try FileManager.default.attributesOfItem(atPath: url.path) + if let modifiedDateInSec = (attributes[.modificationDate] as? Date)?.timeIntervalSince1970 { + return Int(modifiedDateInSec * 1000.0) + } else { + return nil + } + } catch let error as NSError { + Logger.error("getModifiedAtFromUrl failed", error.localizedDescription) + return nil + } + } + + public func getMimeTypeFromUrl(_ url: URL) -> String { + let fileExtension = url.pathExtension as CFString + guard let extUTI = UTTypeCreatePreferredIdentifierForTag(kUTTagClassFilenameExtension, fileExtension, nil)?.takeUnretainedValue() else { + return "" + } + guard let mimeUTI = UTTypeCopyPreferredTagWithClass(extUTI, kUTTagClassMIMEType) else { + return "" + } + return mimeUTI.takeRetainedValue() as String + } + + public func getSizeFromUrl(_ url: URL) throws -> Int { + let values = try url.resourceValues(forKeys: [.fileSizeKey]) + return values.fileSize ?? 0 + } + + public func getVideoDuration(_ url: URL) -> Int { + let asset = AVAsset(url: url) + let duration = asset.duration + let durationTime = CMTimeGetSeconds(duration) + return Int(round(durationTime)) + } + + public func getImageDimensions(_ url: URL) -> (Int?, Int?) { + if let imageSource = CGImageSourceCreateWithURL(url as CFURL, nil) { + if let imageProperties = CGImageSourceCopyPropertiesAtIndex(imageSource, 0, nil) as Dictionary? { + return getHeightAndWidthFromImageProperties(imageProperties) + } + } + return (nil, nil) + } + + public func getVideoDimensions(_ url: URL) -> (Int?, Int?) { + guard let track = AVURLAsset(url: url).tracks(withMediaType: AVMediaType.video).first else { return (nil, nil) } + let size = track.naturalSize.applying(track.preferredTransform) + let height = abs(Int(size.height)) + let width = abs(Int(size.width)) + return (height, width) + } + + private func getHeightAndWidthFromImageProperties(_ properties: [NSObject: AnyObject]) -> (Int?, Int?) { + let width = properties[kCGImagePropertyPixelWidth] as? Int + let height = properties[kCGImagePropertyPixelHeight] as? Int + let orientation = properties[kCGImagePropertyOrientation] as? Int ?? UIImage.Orientation.up.rawValue + switch orientation { + case UIImage.Orientation.left.rawValue, UIImage.Orientation.right.rawValue, UIImage.Orientation.leftMirrored.rawValue, UIImage.Orientation.rightMirrored.rawValue: + return (width, height) + default: + return (height, width) + } + } + + private func getFileUrlByPath(_ path: String) -> URL? { + guard let url = URL.init(string: path) else { + return nil + } + if FileManager.default.fileExists(atPath: url.path) { + return url + } else { + return nil + } + } + + /// ## In which cases do we need to save a copy of a file selected by a user to the app sandbox? + /// In short, only when the file is **not** selected using UIDocumentPickerDelegate. + /// For the rest of the cases, we need to write a copy of the file to the app sandbox. + /// + /// For PHPicker (used for photos and videos), `NSItemProvider.loadFileRepresentation` returns a temporary file URL that is deleted after the completion handler. + /// The recommendation is to [Persist](https://developer.apple.com/documentation/foundation/nsitemprovider/2888338-loadfilerepresentation) the file by moving/copying + /// it to your app’s directory within the completion handler. + /// + /// If available, `loadInPlaceFileRepresentation` can open a file in place; Photos assets typically do not support true in-place access, + /// so fall back to persisting a local file. + /// Ref: https://developer.apple.com/documentation/foundation/nsitemprovider/2888335-loadinplacefilerepresentation + /// + /// For UIDocumentPicker, prefer "open in place" and avoid copying when possible. + /// Ref: https://developer.apple.com/documentation/uikit/uidocumentpickerviewcontroller + private func saveTemporaryFile(_ sourceUrl: URL) throws -> URL { + + var directory = URL(fileURLWithPath: NSTemporaryDirectory()) + if let cachesDirectory = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first { + directory = cachesDirectory + } + + let targetUrl = directory.appendingPathComponent(sourceUrl.lastPathComponent) + do { + try deleteFile(targetUrl) + } + + try FileManager.default.copyItem(at: sourceUrl, to: targetUrl) + return targetUrl + } + + private func deleteFile(_ url: URL) throws { + if FileManager.default.fileExists(atPath: url.path) { + try FileManager.default.removeItem(atPath: url.path) + } + } +} + +extension FilePickerController: UIDocumentPickerDelegate { + public func documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentsAt urls: [URL]) { + do { + self.plugin.onFilePickerEvent(.selected(urls)) + } catch { + self.plugin.onFilePickerEvent(.error("Failed to create a temporary copy of the file")) + } + } + + public func documentPickerWasCancelled(_ controller: UIDocumentPickerViewController) { + self.plugin.onFilePickerEvent(.cancelled) + } +} + +extension FilePickerController: UIImagePickerControllerDelegate, UINavigationControllerDelegate, UIPopoverPresentationControllerDelegate { + public func imagePickerControllerDidCancel(_ picker: UIImagePickerController) { + dismissViewController(picker) + self.plugin.onFilePickerEvent(.cancelled) + } + + public func popoverPresentationControllerDidDismissPopover(_ popoverPresentationController: UIPopoverPresentationController) { + self.plugin.onFilePickerEvent(.cancelled) + } + + public func presentationControllerDidDismiss(_ presentationController: UIPresentationController) { + self.plugin.onFilePickerEvent(.cancelled) + } + + public func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey: Any]) { + dismissViewController(picker) { + if let url = info[.mediaURL] as? URL { + do { + let temporaryUrl = try self.saveTemporaryFile(url) + self.plugin.onFilePickerEvent(.selected([temporaryUrl])) + } catch { + self.plugin.onFilePickerEvent(.error("Failed to create a temporary copy of the file")) + } + } else { + self.plugin.onFilePickerEvent(.cancelled) + } + } + } +} + +@available(iOS 14, *) +extension FilePickerController: PHPickerViewControllerDelegate { + public func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) { + dismissViewController(picker) + if results.first == nil { + self.plugin.onFilePickerEvent(.cancelled) + return + } + var temporaryUrls: [URL] = [] + var errorMessage: String? + let dispatchGroup = DispatchGroup() + for result in results { + if errorMessage != nil { + break + } + if result.itemProvider.hasItemConformingToTypeIdentifier(UTType.movie.identifier) { + dispatchGroup.enter() + result.itemProvider.loadFileRepresentation(forTypeIdentifier: UTType.movie.identifier, completionHandler: { url, error in + defer { + dispatchGroup.leave() + } + if let error = error { + errorMessage = error.localizedDescription + return + } + guard let url = url else { + errorMessage = "Unknown error" + return + } + do { + // We have to make a copy of the file to the app sandbox here, as PHPicker returns an NSItemProvider with either an ephemeral file URL or content that is deleted after the completion handler. + // This is a different behavior from UIDocumentPicker, where the file can either be copied to the app sandbox or opened in place, and then accessed with `startAccessingSecurityScopedResource`. + let temporaryUrl = try self.saveTemporaryFile(url) + temporaryUrls.append(temporaryUrl) + } catch { + errorMessage = "Failed to create a temporary copy of the file" + } + }) + } else if result.itemProvider.hasItemConformingToTypeIdentifier(UTType.image.identifier) { + dispatchGroup.enter() + result.itemProvider.loadFileRepresentation(forTypeIdentifier: UTType.image.identifier, completionHandler: { url, error in + defer { + dispatchGroup.leave() + } + if let error = error { + errorMessage = error.localizedDescription + return + } + guard let url = url else { + errorMessage = "Unknown error" + return + } + do { + // We have to make a copy of the file to the app sandbox here, as PHPicker returns an NSItemProvider with either an ephemeral file URL or content that is deleted after the completion handler. + // This is a different behavior from UIDocumentPicker, where the file can either be copied to the app sandbox or opened in place, and then accessed with `startAccessingSecurityScopedResource`. + let temporaryUrl = try self.saveTemporaryFile(url) + temporaryUrls.append(temporaryUrl) + } catch { + errorMessage = "Failed to create a temporary copy of the file" + } + }) + } else { + errorMessage = "Unsupported file type identifier" + } + } + dispatchGroup.notify(queue: .main) { + if let errorMessage = errorMessage { + self.plugin.onFilePickerEvent(.error(errorMessage)) + return + } + self.plugin.onFilePickerEvent(.selected(temporaryUrls)) + } + } +} \ No newline at end of file diff --git a/plugins/dialog/ios/Tests/PluginTests/PluginTests.swift b/plugins/dialog/ios/Tests/PluginTests/PluginTests.swift new file mode 100644 index 0000000..99992ce --- /dev/null +++ b/plugins/dialog/ios/Tests/PluginTests/PluginTests.swift @@ -0,0 +1,12 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +import XCTest +@testable import ExamplePlugin + +final class ExamplePluginTests: XCTestCase { + func testExample() throws { + let plugin = ExamplePlugin() + } +} diff --git a/plugins/dialog/package.json b/plugins/dialog/package.json new file mode 100644 index 0000000..c7b8d59 --- /dev/null +++ b/plugins/dialog/package.json @@ -0,0 +1,29 @@ +{ + "name": "@tauri-apps/plugin-dialog", + "version": "2.7.3", + "license": "MIT OR Apache-2.0", + "authors": [ + "Tauri Programme within The Commons Conservancy" + ], + "repository": "https://github.com/tauri-apps/plugins-workspace", + "type": "module", + "types": "./dist-js/index.d.ts", + "main": "./dist-js/index.cjs", + "module": "./dist-js/index.js", + "exports": { + "types": "./dist-js/index.d.ts", + "import": "./dist-js/index.js", + "require": "./dist-js/index.cjs" + }, + "scripts": { + "build": "rollup -c" + }, + "files": [ + "dist-js", + "README.md", + "LICENSE" + ], + "dependencies": { + "@tauri-apps/api": "^2.11.0" + } +} diff --git a/plugins/dialog/permissions/ask.toml b/plugins/dialog/permissions/ask.toml new file mode 100644 index 0000000..7f79e84 --- /dev/null +++ b/plugins/dialog/permissions/ask.toml @@ -0,0 +1,11 @@ +"$schema" = "schemas/schema.json" + +[[permission]] +identifier = "allow-ask" +description = "Enables the ask command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `allow-message` and will be removed in v3)" +commands.allow = ["message"] + +[[permission]] +identifier = "deny-ask" +description = "Denies the ask command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `deny-message` and will be removed in v3)" +commands.deny = ["message"] diff --git a/plugins/dialog/permissions/autogenerated/commands/message.toml b/plugins/dialog/permissions/autogenerated/commands/message.toml new file mode 100644 index 0000000..d386d91 --- /dev/null +++ b/plugins/dialog/permissions/autogenerated/commands/message.toml @@ -0,0 +1,13 @@ +# Automatically generated - DO NOT EDIT! + +"$schema" = "../../schemas/schema.json" + +[[permission]] +identifier = "allow-message" +description = "Enables the message command without any pre-configured scope." +commands.allow = ["message"] + +[[permission]] +identifier = "deny-message" +description = "Denies the message command without any pre-configured scope." +commands.deny = ["message"] diff --git a/plugins/dialog/permissions/autogenerated/commands/open.toml b/plugins/dialog/permissions/autogenerated/commands/open.toml new file mode 100644 index 0000000..4ea6dff --- /dev/null +++ b/plugins/dialog/permissions/autogenerated/commands/open.toml @@ -0,0 +1,13 @@ +# Automatically generated - DO NOT EDIT! + +"$schema" = "../../schemas/schema.json" + +[[permission]] +identifier = "allow-open" +description = "Enables the open command without any pre-configured scope." +commands.allow = ["open"] + +[[permission]] +identifier = "deny-open" +description = "Denies the open command without any pre-configured scope." +commands.deny = ["open"] diff --git a/plugins/dialog/permissions/autogenerated/commands/save.toml b/plugins/dialog/permissions/autogenerated/commands/save.toml new file mode 100644 index 0000000..d3e8422 --- /dev/null +++ b/plugins/dialog/permissions/autogenerated/commands/save.toml @@ -0,0 +1,13 @@ +# Automatically generated - DO NOT EDIT! + +"$schema" = "../../schemas/schema.json" + +[[permission]] +identifier = "allow-save" +description = "Enables the save command without any pre-configured scope." +commands.allow = ["save"] + +[[permission]] +identifier = "deny-save" +description = "Denies the save command without any pre-configured scope." +commands.deny = ["save"] diff --git a/plugins/dialog/permissions/autogenerated/reference.md b/plugins/dialog/permissions/autogenerated/reference.md new file mode 100644 index 0000000..b49897d --- /dev/null +++ b/plugins/dialog/permissions/autogenerated/reference.md @@ -0,0 +1,154 @@ +## Default Permission + +This permission set configures the types of dialogs +available from the dialog plugin. + +#### Granted Permissions + +All dialog types are enabled. + +#### This default permission set includes the following: + +- `allow-message` +- `allow-save` +- `allow-open` + +## Permission Table + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
IdentifierDescription
+ +`dialog:allow-ask` + + + +Enables the ask command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `allow-message` and will be removed in v3) + +
+ +`dialog:deny-ask` + + + +Denies the ask command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `deny-message` and will be removed in v3) + +
+ +`dialog:allow-message` + + + +Enables the message command without any pre-configured scope. + +
+ +`dialog:deny-message` + + + +Denies the message command without any pre-configured scope. + +
+ +`dialog:allow-open` + + + +Enables the open command without any pre-configured scope. + +
+ +`dialog:deny-open` + + + +Denies the open command without any pre-configured scope. + +
+ +`dialog:allow-save` + + + +Enables the save command without any pre-configured scope. + +
+ +`dialog:deny-save` + + + +Denies the save command without any pre-configured scope. + +
+ +`dialog:allow-confirm` + + + +Enables the confirm command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `allow-message` and will be removed in v3) + +
+ +`dialog:deny-confirm` + + + +Denies the confirm command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `deny-message` and will be removed in v3) + +
diff --git a/plugins/dialog/permissions/confirm.toml b/plugins/dialog/permissions/confirm.toml new file mode 100644 index 0000000..b03a59b --- /dev/null +++ b/plugins/dialog/permissions/confirm.toml @@ -0,0 +1,11 @@ +"$schema" = "schemas/schema.json" + +[[permission]] +identifier = "allow-confirm" +description = "Enables the confirm command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `allow-message` and will be removed in v3)" +commands.allow = ["message"] + +[[permission]] +identifier = "deny-confirm" +description = "Denies the confirm command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `deny-message` and will be removed in v3)" +commands.deny = ["message"] diff --git a/plugins/dialog/permissions/default.toml b/plugins/dialog/permissions/default.toml new file mode 100644 index 0000000..181e81d --- /dev/null +++ b/plugins/dialog/permissions/default.toml @@ -0,0 +1,14 @@ +"$schema" = "schemas/schema.json" + +[default] +description = """ +This permission set configures the types of dialogs +available from the dialog plugin. + +#### Granted Permissions + +All dialog types are enabled. + + +""" +permissions = ["allow-message", "allow-save", "allow-open"] diff --git a/plugins/dialog/permissions/schemas/schema.json b/plugins/dialog/permissions/schemas/schema.json new file mode 100644 index 0000000..75d98d0 --- /dev/null +++ b/plugins/dialog/permissions/schemas/schema.json @@ -0,0 +1,366 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "PermissionFile", + "description": "Permission file that can define a default permission, a set of permissions or a list of inlined permissions.", + "type": "object", + "properties": { + "default": { + "description": "The default permission set for the plugin", + "anyOf": [ + { + "$ref": "#/definitions/DefaultPermission" + }, + { + "type": "null" + } + ] + }, + "set": { + "description": "A list of permissions sets defined", + "type": "array", + "items": { + "$ref": "#/definitions/PermissionSet" + } + }, + "permission": { + "description": "A list of inlined permissions", + "default": [], + "type": "array", + "items": { + "$ref": "#/definitions/Permission" + } + } + }, + "definitions": { + "DefaultPermission": { + "description": "The default permission set of the plugin.\n\nWorks similarly to a permission with the \"default\" identifier.", + "type": "object", + "required": [ + "permissions" + ], + "properties": { + "version": { + "description": "The version of the permission.", + "type": [ + "integer", + "null" + ], + "format": "uint64", + "minimum": 1.0 + }, + "description": { + "description": "Human-readable description of what the permission does. Tauri convention is to use `

` headings in markdown content for Tauri documentation generation purposes.", + "type": [ + "string", + "null" + ] + }, + "permissions": { + "description": "All permissions this set contains.", + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "PermissionSet": { + "description": "A set of direct permissions grouped together under a new name.", + "type": "object", + "required": [ + "description", + "identifier", + "permissions" + ], + "properties": { + "identifier": { + "description": "A unique identifier for the permission.", + "type": "string" + }, + "description": { + "description": "Human-readable description of what the permission does.", + "type": "string" + }, + "permissions": { + "description": "All permissions this set contains.", + "type": "array", + "items": { + "$ref": "#/definitions/PermissionKind" + } + } + } + }, + "Permission": { + "description": "Descriptions of explicit privileges of commands.\n\nIt can enable commands to be accessible in the frontend of the application.\n\nIf the scope is defined it can be used to fine grain control the access of individual or multiple commands.", + "type": "object", + "required": [ + "identifier" + ], + "properties": { + "version": { + "description": "The version of the permission.", + "type": [ + "integer", + "null" + ], + "format": "uint64", + "minimum": 1.0 + }, + "identifier": { + "description": "A unique identifier for the permission.", + "type": "string" + }, + "description": { + "description": "Human-readable description of what the permission does. Tauri internal convention is to use `

` headings in markdown content for Tauri documentation generation purposes.", + "type": [ + "string", + "null" + ] + }, + "commands": { + "description": "Allowed or denied commands when using this permission.", + "default": { + "allow": [], + "deny": [] + }, + "allOf": [ + { + "$ref": "#/definitions/Commands" + } + ] + }, + "scope": { + "description": "Allowed or denied scoped when using this permission.", + "allOf": [ + { + "$ref": "#/definitions/Scopes" + } + ] + }, + "platforms": { + "description": "Target platforms this permission applies. By default all platforms are affected by this permission.", + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/Target" + } + } + } + }, + "Commands": { + "description": "Allowed and denied commands inside a permission.\n\nIf two commands clash inside of `allow` and `deny`, it should be denied by default.", + "type": "object", + "properties": { + "allow": { + "description": "Allowed command.", + "default": [], + "type": "array", + "items": { + "type": "string" + } + }, + "deny": { + "description": "Denied command, which takes priority.", + "default": [], + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "Scopes": { + "description": "An argument for fine grained behavior control of Tauri commands.\n\nIt can be of any serde serializable type and is used to allow or prevent certain actions inside a Tauri command. The configured scope is passed to the command and will be enforced by the command implementation.\n\n## Example\n\n```json { \"allow\": [{ \"path\": \"$HOME/**\" }], \"deny\": [{ \"path\": \"$HOME/secret.txt\" }] } ```", + "type": "object", + "properties": { + "allow": { + "description": "Data that defines what is allowed by the scope.", + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/Value" + } + }, + "deny": { + "description": "Data that defines what is denied by the scope. This should be prioritized by validation logic.", + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/Value" + } + } + } + }, + "Value": { + "description": "All supported ACL values.", + "anyOf": [ + { + "description": "Represents a null JSON value.", + "type": "null" + }, + { + "description": "Represents a [`bool`].", + "type": "boolean" + }, + { + "description": "Represents a valid ACL [`Number`].", + "allOf": [ + { + "$ref": "#/definitions/Number" + } + ] + }, + { + "description": "Represents a [`String`].", + "type": "string" + }, + { + "description": "Represents a list of other [`Value`]s.", + "type": "array", + "items": { + "$ref": "#/definitions/Value" + } + }, + { + "description": "Represents a map of [`String`] keys to [`Value`]s.", + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/Value" + } + } + ] + }, + "Number": { + "description": "A valid ACL number.", + "anyOf": [ + { + "description": "Represents an [`i64`].", + "type": "integer", + "format": "int64" + }, + { + "description": "Represents a [`f64`].", + "type": "number", + "format": "double" + } + ] + }, + "Target": { + "description": "Platform target.", + "oneOf": [ + { + "description": "MacOS.", + "type": "string", + "enum": [ + "macOS" + ] + }, + { + "description": "Windows.", + "type": "string", + "enum": [ + "windows" + ] + }, + { + "description": "Linux.", + "type": "string", + "enum": [ + "linux" + ] + }, + { + "description": "Android.", + "type": "string", + "enum": [ + "android" + ] + }, + { + "description": "iOS.", + "type": "string", + "enum": [ + "iOS" + ] + } + ] + }, + "PermissionKind": { + "type": "string", + "oneOf": [ + { + "description": "Enables the ask command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `allow-message` and will be removed in v3)", + "type": "string", + "const": "allow-ask", + "markdownDescription": "Enables the ask command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `allow-message` and will be removed in v3)" + }, + { + "description": "Denies the ask command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `deny-message` and will be removed in v3)", + "type": "string", + "const": "deny-ask", + "markdownDescription": "Denies the ask command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `deny-message` and will be removed in v3)" + }, + { + "description": "Enables the message command without any pre-configured scope.", + "type": "string", + "const": "allow-message", + "markdownDescription": "Enables the message command without any pre-configured scope." + }, + { + "description": "Denies the message command without any pre-configured scope.", + "type": "string", + "const": "deny-message", + "markdownDescription": "Denies the message command without any pre-configured scope." + }, + { + "description": "Enables the open command without any pre-configured scope.", + "type": "string", + "const": "allow-open", + "markdownDescription": "Enables the open command without any pre-configured scope." + }, + { + "description": "Denies the open command without any pre-configured scope.", + "type": "string", + "const": "deny-open", + "markdownDescription": "Denies the open command without any pre-configured scope." + }, + { + "description": "Enables the save command without any pre-configured scope.", + "type": "string", + "const": "allow-save", + "markdownDescription": "Enables the save command without any pre-configured scope." + }, + { + "description": "Denies the save command without any pre-configured scope.", + "type": "string", + "const": "deny-save", + "markdownDescription": "Denies the save command without any pre-configured scope." + }, + { + "description": "Enables the confirm command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `allow-message` and will be removed in v3)", + "type": "string", + "const": "allow-confirm", + "markdownDescription": "Enables the confirm command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `allow-message` and will be removed in v3)" + }, + { + "description": "Denies the confirm command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `deny-message` and will be removed in v3)", + "type": "string", + "const": "deny-confirm", + "markdownDescription": "Denies the confirm command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `deny-message` and will be removed in v3)" + }, + { + "description": "This permission set configures the types of dialogs\navailable from the dialog plugin.\n\n#### Granted Permissions\n\nAll dialog types are enabled.\n\n\n\n#### This default permission set includes:\n\n- `allow-message`\n- `allow-save`\n- `allow-open`", + "type": "string", + "const": "default", + "markdownDescription": "This permission set configures the types of dialogs\navailable from the dialog plugin.\n\n#### Granted Permissions\n\nAll dialog types are enabled.\n\n\n\n#### This default permission set includes:\n\n- `allow-message`\n- `allow-save`\n- `allow-open`" + } + ] + } + } +} \ No newline at end of file diff --git a/plugins/dialog/rollup.config.js b/plugins/dialog/rollup.config.js new file mode 100644 index 0000000..a7dbd4f --- /dev/null +++ b/plugins/dialog/rollup.config.js @@ -0,0 +1,22 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +import { createConfig } from '../../shared/rollup.config.js' +import { nodeResolve } from '@rollup/plugin-node-resolve' +import typescript from '@rollup/plugin-typescript' +import terser from '@rollup/plugin-terser' + +export default createConfig({ + additionalConfigs: { + input: 'guest-js/init.ts', + output: { + file: 'src/init-iife.js', + format: 'iife' + }, + plugins: [typescript(), terser(), nodeResolve()], + onwarn: (warning) => { + throw Object.assign(new Error(), warning) + } + } +}) diff --git a/plugins/dialog/src/commands.rs b/plugins/dialog/src/commands.rs new file mode 100644 index 0000000..2564a1a --- /dev/null +++ b/plugins/dialog/src/commands.rs @@ -0,0 +1,290 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use std::path::PathBuf; + +use serde::{Deserialize, Serialize}; +use tauri::{command, Manager, Runtime, State, Window}; +use tauri_plugin_fs::FsExt; + +use crate::{ + Dialog, FileAccessMode, FileDialogBuilder, FilePath, MessageDialogButtons, MessageDialogKind, + MessageDialogResult, PickerMode, Result, +}; + +#[derive(Serialize)] +#[serde(untagged)] +pub enum OpenResponse { + #[cfg(desktop)] + Folders(Option>), + #[cfg(desktop)] + Folder(Option), + Files(Option>), + File(Option), +} + +#[allow(dead_code)] +#[derive(Debug, Clone, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct DialogFilter { + name: String, + extensions: Vec, +} + +/// The options for the open dialog API. +#[derive(Debug, Clone, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct OpenDialogOptions { + /// The title of the dialog window. + title: Option, + /// The filters of the dialog. + #[serde(default)] + filters: Vec, + /// Whether the dialog allows multiple selection or not. + #[serde(default)] + multiple: bool, + /// Whether the dialog is a directory selection (`true` value) or file selection (`false` value). + #[serde(default)] + directory: bool, + /// The initial path of the dialog. + default_path: Option, + /// If [`Self::directory`] is true, indicates that it will be read recursively later. + /// Defines whether subdirectories will be allowed on the scope or not. + #[serde(default)] + #[cfg_attr(mobile, allow(dead_code))] + recursive: bool, + /// Whether to allow creating directories in the dialog **macOS Only** + can_create_directories: Option, + /// The preferred mode of the dialog. + /// This is meant for mobile platforms (iOS and Android) which have distinct file and media pickers. + /// On desktop, this option is ignored. + /// If not provided, the dialog will automatically choose the best mode based on the MIME types of the filters. + #[serde(default)] + #[cfg_attr(mobile, allow(dead_code))] + picker_mode: Option, + /// The file access mode of the dialog. + #[serde(default)] + #[cfg_attr(mobile, allow(dead_code))] + file_access_mode: Option, +} + +/// The options for the save dialog API. +#[derive(Debug, Clone, Deserialize)] +#[serde(rename_all = "camelCase")] +#[cfg_attr(mobile, allow(dead_code))] +pub struct SaveDialogOptions { + /// The title of the dialog window. + title: Option, + /// The filters of the dialog. + #[serde(default)] + filters: Vec, + /// The initial path of the dialog. + default_path: Option, + /// Whether to allow creating directories in the dialog **macOS Only** + can_create_directories: Option, +} + +#[cfg(mobile)] +fn set_default_path( + mut dialog_builder: FileDialogBuilder, + default_path: PathBuf, +) -> FileDialogBuilder { + if let Some(file_name) = default_path.file_name() { + dialog_builder = dialog_builder.set_file_name(file_name.to_string_lossy()); + } + dialog_builder +} + +#[cfg(desktop)] +fn set_default_path( + mut dialog_builder: FileDialogBuilder, + default_path: PathBuf, +) -> FileDialogBuilder { + // we need to adjust the separator on Windows: https://github.com/tauri-apps/tauri/issues/8074 + let default_path: PathBuf = default_path.components().collect(); + if default_path.is_file() || !default_path.exists() { + if let (Some(parent), Some(file_name)) = (default_path.parent(), default_path.file_name()) { + if parent.components().count() > 0 { + dialog_builder = dialog_builder.set_directory(parent); + } + dialog_builder = dialog_builder.set_file_name(file_name.to_string_lossy()); + } else { + dialog_builder = dialog_builder.set_directory(default_path); + } + dialog_builder + } else { + dialog_builder.set_directory(default_path) + } +} + +#[command] +pub(crate) async fn open( + window: Window, + dialog: State<'_, Dialog>, + options: OpenDialogOptions, +) -> Result { + let mut dialog_builder = dialog.file(); + #[cfg(any(windows, target_os = "macos"))] + { + dialog_builder = dialog_builder.set_parent(&window); + } + if let Some(title) = options.title { + dialog_builder = dialog_builder.set_title(title); + } + if let Some(default_path) = options.default_path { + dialog_builder = set_default_path(dialog_builder, default_path); + } + if let Some(can) = options.can_create_directories { + dialog_builder = dialog_builder.set_can_create_directories(can); + } + if let Some(picker_mode) = options.picker_mode { + dialog_builder = dialog_builder.set_picker_mode(picker_mode); + } + for filter in options.filters { + let extensions: Vec<&str> = filter.extensions.iter().map(|s| &**s).collect(); + dialog_builder = dialog_builder.add_filter(filter.name, &extensions); + } + if let Some(file_access_mode) = options.file_access_mode { + dialog_builder = dialog_builder.set_file_access_mode(file_access_mode); + } + + let res = if options.directory { + #[cfg(desktop)] + { + let tauri_scope = window.state::(); + + if options.multiple { + let folders = dialog_builder.blocking_pick_folders(); + if let Some(folders) = &folders { + for folder in folders { + if let Ok(path) = folder.clone().into_path() { + if let Some(s) = window.try_fs_scope() { + s.allow_directory(&path, options.recursive)?; + } + tauri_scope.allow_directory(&path, options.directory)?; + } + } + } + OpenResponse::Folders( + folders.map(|folders| folders.into_iter().map(|p| p.simplified()).collect()), + ) + } else { + let folder = dialog_builder.blocking_pick_folder(); + if let Some(folder) = &folder { + if let Ok(path) = folder.clone().into_path() { + if let Some(s) = window.try_fs_scope() { + s.allow_directory(&path, options.recursive)?; + } + tauri_scope.allow_directory(&path, options.directory)?; + } + } + OpenResponse::Folder(folder.map(|p| p.simplified())) + } + } + #[cfg(mobile)] + return Err(crate::Error::FolderPickerNotImplemented); + } else if options.multiple { + let tauri_scope = window.state::(); + + let files = dialog_builder.blocking_pick_files(); + if let Some(files) = &files { + for file in files { + if let Ok(path) = file.clone().into_path() { + if let Some(s) = window.try_fs_scope() { + s.allow_file(&path)?; + } + + tauri_scope.allow_file(&path)?; + } + } + } + OpenResponse::Files(files.map(|files| files.into_iter().map(|f| f.simplified()).collect())) + } else { + let tauri_scope = window.state::(); + let file = dialog_builder.blocking_pick_file(); + + if let Some(file) = &file { + if let Ok(path) = file.clone().into_path() { + if let Some(s) = window.try_fs_scope() { + s.allow_file(&path)?; + } + tauri_scope.allow_file(&path)?; + } + } + OpenResponse::File(file.map(|f| f.simplified())) + }; + Ok(res) +} + +#[allow(unused_variables)] +#[command] +pub(crate) async fn save( + window: Window, + dialog: State<'_, Dialog>, + options: SaveDialogOptions, +) -> Result> { + let mut dialog_builder = dialog.file(); + #[cfg(desktop)] + { + dialog_builder = dialog_builder.set_parent(&window); + } + if let Some(title) = options.title { + dialog_builder = dialog_builder.set_title(title); + } + if let Some(default_path) = options.default_path { + dialog_builder = set_default_path(dialog_builder, default_path); + } + if let Some(can) = options.can_create_directories { + dialog_builder = dialog_builder.set_can_create_directories(can); + } + for filter in options.filters { + let extensions: Vec<&str> = filter.extensions.iter().map(|s| &**s).collect(); + dialog_builder = dialog_builder.add_filter(filter.name, &extensions); + } + + let tauri_scope = window.state::(); + + let path = dialog_builder.blocking_save_file(); + if let Some(p) = &path { + if let Ok(path) = p.clone().into_path() { + if let Some(s) = window.try_fs_scope() { + s.allow_file(&path)?; + } + tauri_scope.allow_file(&path)?; + } + } + + Ok(path.map(|p| p.simplified())) +} + +#[command] +pub(crate) async fn message( + #[allow(unused)] window: Window, + dialog: State<'_, Dialog>, + title: Option, + message: String, + kind: Option, + buttons: Option, +) -> Result { + let mut builder = dialog.message(message); + + if let Some(buttons) = buttons { + builder = builder.buttons(buttons); + } + + if let Some(title) = title { + builder = builder.title(title); + } + + #[cfg(desktop)] + { + builder = builder.parent(&window); + } + + if let Some(kind) = kind { + builder = builder.kind(kind); + } + + Ok(builder.blocking_show_with_result()) +} diff --git a/plugins/dialog/src/desktop.rs b/plugins/dialog/src/desktop.rs new file mode 100644 index 0000000..8d3b08f --- /dev/null +++ b/plugins/dialog/src/desktop.rs @@ -0,0 +1,257 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +//! Use native message and file open/save dialogs. +//! +//! This module exposes non-blocking APIs on its root, relying on callback closures +//! to give results back. This is particularly useful when running dialogs from the main thread. +//! When using on asynchronous contexts such as async commands, the [`blocking`] APIs are recommended. + +use raw_window_handle::{HasDisplayHandle, HasWindowHandle, RawDisplayHandle, RawWindowHandle}; +use rfd::{AsyncFileDialog, AsyncMessageDialog}; +use serde::de::DeserializeOwned; +use tauri::{plugin::PluginApi, AppHandle, Runtime}; + +use crate::{models::*, FileDialogBuilder, FilePath, MessageDialogBuilder}; + +pub fn init( + app: &AppHandle, + _api: PluginApi, +) -> crate::Result> { + Ok(Dialog(app.clone())) +} + +/// Access to the dialog APIs. +#[derive(Debug)] +pub struct Dialog(AppHandle); + +impl Clone for Dialog { + fn clone(&self) -> Self { + Self(self.0.clone()) + } +} + +impl Dialog { + pub(crate) fn app_handle(&self) -> &AppHandle { + &self.0 + } +} + +impl From for rfd::MessageLevel { + fn from(kind: MessageDialogKind) -> Self { + match kind { + MessageDialogKind::Info => Self::Info, + MessageDialogKind::Warning => Self::Warning, + MessageDialogKind::Error => Self::Error, + } + } +} + +#[derive(Debug)] +pub(crate) struct WindowHandle { + window_handle: RawWindowHandle, + display_handle: RawDisplayHandle, +} + +impl WindowHandle { + pub(crate) fn new(window_handle: RawWindowHandle, display_handle: RawDisplayHandle) -> Self { + Self { + window_handle, + display_handle, + } + } +} + +impl HasWindowHandle for WindowHandle { + fn window_handle( + &self, + ) -> Result, raw_window_handle::HandleError> { + Ok(unsafe { raw_window_handle::WindowHandle::borrow_raw(self.window_handle) }) + } +} + +impl HasDisplayHandle for WindowHandle { + fn display_handle( + &self, + ) -> Result, raw_window_handle::HandleError> { + Ok(unsafe { raw_window_handle::DisplayHandle::borrow_raw(self.display_handle) }) + } +} + +impl From> for AsyncFileDialog { + fn from(d: FileDialogBuilder) -> Self { + let mut builder = AsyncFileDialog::new(); + + if let Some(title) = d.title { + builder = builder.set_title(title); + } + if let Some(starting_directory) = d.starting_directory { + builder = builder.set_directory(starting_directory); + } + if let Some(file_name) = d.file_name { + builder = builder.set_file_name(file_name); + } + for filter in d.filters { + let v: Vec<&str> = filter.extensions.iter().map(|x| &**x).collect(); + builder = builder.add_filter(&filter.name, &v); + } + #[cfg(desktop)] + if let Some(parent) = d.parent { + builder = builder.set_parent(&parent); + } + + builder = builder.set_can_create_directories(d.can_create_directories.unwrap_or(true)); + + builder + } +} + +impl From for rfd::MessageButtons { + fn from(value: MessageDialogButtons) -> Self { + match value { + MessageDialogButtons::Ok => Self::Ok, + MessageDialogButtons::OkCancel => Self::OkCancel, + MessageDialogButtons::YesNo => Self::YesNo, + MessageDialogButtons::OkCustom(ok) => Self::OkCustom(ok), + MessageDialogButtons::OkCancelCustom(ok, cancel) => Self::OkCancelCustom(ok, cancel), + MessageDialogButtons::YesNoCancel => Self::YesNoCancel, + MessageDialogButtons::YesNoCancelCustom(yes, no, cancel) => { + Self::YesNoCancelCustom(yes, no, cancel) + } + } + } +} + +impl From> for AsyncMessageDialog { + fn from(d: MessageDialogBuilder) -> Self { + let mut dialog = AsyncMessageDialog::new() + .set_title(&d.title) + .set_description(&d.message) + .set_level(d.kind.into()) + .set_buttons(d.buttons.into()); + + if let Some(parent) = d.parent { + dialog = dialog.set_parent(&parent); + } + + dialog + } +} + +pub fn pick_file) + Send + 'static>( + dialog: FileDialogBuilder, + f: F, +) { + let f = |path: Option| f(path.map(|p| p.path().to_path_buf().into())); + let handle = dialog.dialog.app_handle().to_owned(); + let _ = handle.run_on_main_thread(move || { + let dialog = AsyncFileDialog::from(dialog).pick_file(); + std::thread::spawn(move || f(tauri::async_runtime::block_on(dialog))); + }); +} + +pub fn pick_files>) + Send + 'static>( + dialog: FileDialogBuilder, + f: F, +) { + let f = |paths: Option>| { + f(paths.map(|list| { + list.into_iter() + .map(|p| p.path().to_path_buf().into()) + .collect() + })) + }; + let handle = dialog.dialog.app_handle().to_owned(); + let _ = handle.run_on_main_thread(move || { + let dialog = AsyncFileDialog::from(dialog).pick_files(); + std::thread::spawn(move || f(tauri::async_runtime::block_on(dialog))); + }); +} + +pub fn pick_folder) + Send + 'static>( + dialog: FileDialogBuilder, + f: F, +) { + let f = |path: Option| f(path.map(|p| p.path().to_path_buf().into())); + let handle = dialog.dialog.app_handle().to_owned(); + let _ = handle.run_on_main_thread(move || { + let dialog = AsyncFileDialog::from(dialog).pick_folder(); + std::thread::spawn(move || f(tauri::async_runtime::block_on(dialog))); + }); +} + +pub fn pick_folders>) + Send + 'static>( + dialog: FileDialogBuilder, + f: F, +) { + let f = |paths: Option>| { + f(paths.map(|list| { + list.into_iter() + .map(|p| p.path().to_path_buf().into()) + .collect() + })) + }; + let handle = dialog.dialog.app_handle().to_owned(); + let _ = handle.run_on_main_thread(move || { + let dialog = AsyncFileDialog::from(dialog).pick_folders(); + std::thread::spawn(move || f(tauri::async_runtime::block_on(dialog))); + }); +} + +pub fn save_file) + Send + 'static>( + dialog: FileDialogBuilder, + f: F, +) { + let f = |path: Option| f(path.map(|p| p.path().to_path_buf().into())); + let handle = dialog.dialog.app_handle().to_owned(); + let _ = handle.run_on_main_thread(move || { + let dialog = AsyncFileDialog::from(dialog).save_file(); + std::thread::spawn(move || f(tauri::async_runtime::block_on(dialog))); + }); +} + +/// Shows a message dialog +pub fn show_message_dialog( + dialog: MessageDialogBuilder, + callback: F, +) { + let f = move |res: rfd::MessageDialogResult| callback(res.into()); + + let handle = dialog.dialog.app_handle().to_owned(); + let _ = handle.run_on_main_thread(move || { + let buttons = dialog.buttons.clone(); + let dialog = AsyncMessageDialog::from(dialog).show(); + std::thread::spawn(move || { + let result = tauri::async_runtime::block_on(dialog); + // on Linux rfd does not return rfd::MessageDialogResult::Custom, so we must map manually + let result = match (result, buttons) { + (rfd::MessageDialogResult::Ok, MessageDialogButtons::OkCustom(s)) => { + rfd::MessageDialogResult::Custom(s) + } + ( + rfd::MessageDialogResult::Ok, + MessageDialogButtons::OkCancelCustom(ok, _cancel), + ) => rfd::MessageDialogResult::Custom(ok), + ( + rfd::MessageDialogResult::Cancel, + MessageDialogButtons::OkCancelCustom(_ok, cancel), + ) => rfd::MessageDialogResult::Custom(cancel), + ( + rfd::MessageDialogResult::Yes, + MessageDialogButtons::YesNoCancelCustom(yes, _no, _cancel), + ) => rfd::MessageDialogResult::Custom(yes), + ( + rfd::MessageDialogResult::No, + MessageDialogButtons::YesNoCancelCustom(_yes, no, _cancel), + ) => rfd::MessageDialogResult::Custom(no), + ( + rfd::MessageDialogResult::Cancel, + MessageDialogButtons::YesNoCancelCustom(_yes, _no, cancel), + ) => rfd::MessageDialogResult::Custom(cancel), + (result, _) => result, + }; + f(result); + }); + }); +} diff --git a/plugins/dialog/src/error.rs b/plugins/dialog/src/error.rs new file mode 100644 index 0000000..dac05b1 --- /dev/null +++ b/plugins/dialog/src/error.rs @@ -0,0 +1,40 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use serde::{ser::Serializer, Serialize}; + +/// Alias for `Result` used throughout this crate. +pub type Result = std::result::Result; + +/// Errors that can occur while showing or interacting with a dialog. +#[derive(Debug, thiserror::Error)] +#[non_exhaustive] +pub enum Error { + /// An error forwarded from the Tauri core. + #[error(transparent)] + Tauri(#[from] tauri::Error), + /// An I/O error, for example while resolving a picked path. + #[error(transparent)] + Io(#[from] std::io::Error), + /// Forwarding a request to, or receiving a response from, the mobile plugin failed. + #[cfg(mobile)] + #[error(transparent)] + PluginInvoke(#[from] tauri::plugin::mobile::PluginInvokeError), + /// The folder picker was requested through the `open` command, but folder picking is not implemented on mobile. + #[cfg(mobile)] + #[error("Folder picker is not implemented on mobile")] + FolderPickerNotImplemented, + /// An error forwarded from the `fs` plugin, returned when granting filesystem scope to a picked path fails. + #[error(transparent)] + Fs(#[from] tauri_plugin_fs::Error), +} + +impl Serialize for Error { + fn serialize(&self, serializer: S) -> std::result::Result + where + S: Serializer, + { + serializer.serialize_str(self.to_string().as_ref()) + } +} diff --git a/plugins/dialog/src/init-iife.js b/plugins/dialog/src/init-iife.js new file mode 100644 index 0000000..bcdf3f5 --- /dev/null +++ b/plugins/dialog/src/init-iife.js @@ -0,0 +1 @@ +!function(){"use strict";async function n(n,i={},o){return window.__TAURI_INTERNALS__.invoke(n,i,o)}"function"==typeof SuppressedError&&SuppressedError,window.alert=function(i){n("plugin:dialog|message",{message:i.toString()})},window.confirm=async function(i){return await n("plugin:dialog|confirm",{message:i.toString()})}}(); diff --git a/plugins/dialog/src/lib.rs b/plugins/dialog/src/lib.rs new file mode 100644 index 0000000..0e092af --- /dev/null +++ b/plugins/dialog/src/lib.rs @@ -0,0 +1,786 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +//! Native system dialogs for opening and saving files along with message dialogs. +//! +//! ## Cargo features +//! +//! - **gtk3** *(enabled by default)*: Uses GTK for dialogs on Linux & BSDs; has no effect on Windows and macOS +//! - **xdg-portal**: Uses XDG Desktop Portal instead of GTK on Linux & BSDs + +#![doc( + html_logo_url = "https://github.com/tauri-apps/tauri/raw/dev/app-icon.png", + html_favicon_url = "https://github.com/tauri-apps/tauri/raw/dev/app-icon.png" +)] + +use serde::{Deserialize, Serialize}; +use tauri::{ + plugin::{Builder, TauriPlugin}, + Manager, Runtime, +}; + +use std::{ + path::{Path, PathBuf}, + sync::mpsc::sync_channel, +}; + +pub use models::*; + +pub use tauri_plugin_fs::FilePath; +#[cfg(desktop)] +mod desktop; +#[cfg(mobile)] +mod mobile; + +mod commands; +mod error; +mod models; + +pub use error::{Error, Result}; + +#[cfg(desktop)] +use desktop::*; +#[cfg(mobile)] +use mobile::*; + +#[cfg(desktop)] +pub use desktop::Dialog; +#[cfg(mobile)] +pub use mobile::Dialog; + +/// The preferred mode of the file picker on mobile platforms (iOS and Android), which have +/// distinct file and media pickers. On desktop, this option is ignored. +#[derive(Debug, Serialize, Deserialize, Clone)] +#[serde(rename_all = "lowercase")] +pub enum PickerMode { + /// Show the generic document picker. + Document, + /// Show the media picker, allowing both images and videos to be selected. + Media, + /// Show the media picker restricted to images. + Image, + /// Show the media picker restricted to videos. + Video, +} + +/// The file access mode of the dialog, used to control how a picked file is exposed to the app on iOS. +#[derive(Debug, Serialize, Deserialize, Clone)] +#[serde(rename_all = "lowercase")] +pub enum FileAccessMode { + /// Copy the picked file into the app's sandbox so it can be freely read, edited or deleted. + Copy, + /// Keep the file at its original location and let the system manage security-scoped access to it. + Scoped, +} + +pub(crate) const OK: &str = "Ok"; +#[cfg(mobile)] +pub(crate) const CANCEL: &str = "Cancel"; +#[cfg(mobile)] +pub(crate) const YES: &str = "Yes"; +#[cfg(mobile)] +pub(crate) const NO: &str = "No"; + +macro_rules! blocking_fn { + ($self:ident, $fn:ident) => {{ + let (tx, rx) = sync_channel(0); + let cb = move |response| { + tx.send(response).unwrap(); + }; + $self.$fn(cb); + rx.recv().unwrap() + }}; +} + +/// Extensions to [`tauri::App`], [`tauri::AppHandle`], [`tauri::WebviewWindow`], [`tauri::Webview`] and [`tauri::Window`] to access the dialog APIs. +pub trait DialogExt { + /// Returns the [`Dialog`] instance associated with this app/window. + fn dialog(&self) -> &Dialog; +} + +impl> crate::DialogExt for T { + fn dialog(&self) -> &Dialog { + self.state::>().inner() + } +} + +impl Dialog { + /// Create a new messaging dialog builder. + /// The dialog can optionally ask the user for confirmation or include an OK button. + /// + /// # Examples + /// + /// - Message dialog: + /// + /// ```no_run + /// use tauri_plugin_dialog::DialogExt; + /// + /// tauri::Builder::default() + /// .setup(|app| { + /// app + /// .dialog() + /// .message("Tauri is Awesome!") + /// .show(|_| { + /// println!("dialog closed"); + /// }); + /// Ok(()) + /// }); + /// ``` + /// + /// - Ask dialog: + /// + /// ```no_run + /// use tauri_plugin_dialog::{DialogExt, MessageDialogButtons}; + /// + /// tauri::Builder::default() + /// .setup(|app| { + /// app.dialog() + /// .message("Are you sure?") + /// .buttons(MessageDialogButtons::OkCancelCustom("Yes".to_string(), "No".to_string())) + /// .show(|yes| { + /// println!("user said {}", if yes { "yes" } else { "no" }); + /// }); + /// Ok(()) + /// }); + /// ``` + /// + /// - Message dialog with OK button: + /// + /// ```no_run + /// use tauri_plugin_dialog::{DialogExt, MessageDialogButtons}; + /// + /// tauri::Builder::default() + /// .setup(|app| { + /// app.dialog() + /// .message("Job completed successfully") + /// .buttons(MessageDialogButtons::Ok) + /// .show(|_| { + /// println!("dialog closed"); + /// }); + /// Ok(()) + /// }); + /// ``` + /// + /// # `show` vs `blocking_show` + /// + /// The dialog builder includes two separate APIs for rendering the dialog: `show` and `blocking_show`. + /// The `show` function is asynchronous and takes a closure to be executed when the dialog is closed. + /// To block the current thread until the user acted on the dialog, you can use `blocking_show`, + /// but note that it cannot be executed on the main thread as it will freeze your application. + /// + /// ```no_run + /// use tauri_plugin_dialog::{DialogExt, MessageDialogButtons}; + /// + /// tauri::Builder::default() + /// .setup(|app| { + /// let handle = app.handle().clone(); + /// std::thread::spawn(move || { + /// let yes = handle.dialog() + /// .message("Are you sure?") + /// .buttons(MessageDialogButtons::OkCancelCustom("Yes".to_string(), "No".to_string())) + /// .blocking_show(); + /// }); + /// + /// Ok(()) + /// }); + /// ``` + pub fn message(&self, message: impl Into) -> MessageDialogBuilder { + MessageDialogBuilder::new( + self.clone(), + self.app_handle().package_info().name.clone(), + message, + ) + } + + /// Creates a new builder for dialogs that lets the user select file(s) or folder(s). + pub fn file(&self) -> FileDialogBuilder { + FileDialogBuilder::new(self.clone()) + } +} + +/// Initializes the plugin. +pub fn init() -> TauriPlugin { + #[allow(unused_mut)] + let mut builder = Builder::new("dialog"); + + // Dialogs are implemented natively on Android + #[cfg(not(target_os = "android"))] + { + builder = builder.js_init_script(include_str!("init-iife.js").to_string()); + } + + builder + .invoke_handler(tauri::generate_handler![ + commands::open, + commands::save, + commands::message, + ]) + .setup(|app, api| { + #[cfg(mobile)] + let dialog = mobile::init(app, api)?; + #[cfg(desktop)] + let dialog = desktop::init(app, api)?; + app.manage(dialog); + Ok(()) + }) + .build() +} + +/// A builder for message dialogs. +pub struct MessageDialogBuilder { + #[allow(dead_code)] + pub(crate) dialog: Dialog, + pub(crate) title: String, + pub(crate) message: String, + pub(crate) kind: MessageDialogKind, + pub(crate) buttons: MessageDialogButtons, + #[cfg(desktop)] + pub(crate) parent: Option, +} + +/// Payload for the message dialog mobile API. +#[cfg(mobile)] +#[derive(Serialize)] +#[serde(rename_all = "camelCase")] +pub(crate) struct MessageDialogPayload<'a> { + title: &'a String, + message: &'a String, + kind: &'a MessageDialogKind, + ok_button_label: Option<&'a str>, + no_button_label: Option<&'a str>, + cancel_button_label: Option<&'a str>, +} + +// raw window handle :( +unsafe impl Send for MessageDialogBuilder {} + +impl MessageDialogBuilder { + /// Creates a new message dialog builder. + pub fn new(dialog: Dialog, title: impl Into, message: impl Into) -> Self { + Self { + dialog, + title: title.into(), + message: message.into(), + kind: MessageDialogKind::default(), + buttons: MessageDialogButtons::default(), + #[cfg(desktop)] + parent: None, + } + } + + #[cfg(mobile)] + pub(crate) fn payload(&self) -> MessageDialogPayload<'_> { + let (ok_button_label, no_button_label, cancel_button_label) = match &self.buttons { + MessageDialogButtons::Ok => (Some(OK), None, None), + MessageDialogButtons::OkCancel => (Some(OK), None, Some(CANCEL)), + MessageDialogButtons::YesNo => (Some(YES), Some(NO), None), + MessageDialogButtons::YesNoCancel => (Some(YES), Some(NO), Some(CANCEL)), + MessageDialogButtons::OkCustom(ok) => (Some(ok.as_str()), None, None), + MessageDialogButtons::OkCancelCustom(ok, cancel) => { + (Some(ok.as_str()), None, Some(cancel.as_str())) + } + MessageDialogButtons::YesNoCancelCustom(yes, no, cancel) => { + (Some(yes.as_str()), Some(no.as_str()), Some(cancel.as_str())) + } + }; + MessageDialogPayload { + title: &self.title, + message: &self.message, + kind: &self.kind, + ok_button_label, + no_button_label, + cancel_button_label, + } + } + + /// Sets the dialog title. + pub fn title(mut self, title: impl Into) -> Self { + self.title = title.into(); + self + } + + /// Set parent windows explicitly (optional) + #[cfg(desktop)] + pub fn parent( + mut self, + parent: &W, + ) -> Self { + if let (Ok(window_handle), Ok(display_handle)) = + (parent.window_handle(), parent.display_handle()) + { + self.parent.replace(crate::desktop::WindowHandle::new( + window_handle.as_raw(), + display_handle.as_raw(), + )); + } + self + } + + /// Sets the dialog buttons. + pub fn buttons(mut self, buttons: MessageDialogButtons) -> Self { + self.buttons = buttons; + self + } + + /// Set type of a dialog. + /// + /// Depending on the system it can result in type specific icon to show up, + /// the will inform user it message is a error, warning or just information. + pub fn kind(mut self, kind: MessageDialogKind) -> Self { + self.kind = kind; + self + } + + /// Shows a message dialog + /// + /// Returns `true` if the user pressed the OK/Yes button, + pub fn show(self, f: F) { + let ok_label = match &self.buttons { + MessageDialogButtons::OkCustom(ok) => Some(ok.clone()), + MessageDialogButtons::OkCancelCustom(ok, _) => Some(ok.clone()), + MessageDialogButtons::YesNoCancelCustom(yes, _, _) => Some(yes.clone()), + _ => None, + }; + + show_message_dialog(self, move |res| { + let sucess = match res { + MessageDialogResult::Ok | MessageDialogResult::Yes => true, + MessageDialogResult::Custom(s) => { + ok_label.map_or(s == OK, |ok_label| ok_label == s) + } + _ => false, + }; + + f(sucess) + }) + } + + /// Shows a message dialog and returns the button that was pressed. + /// + /// Returns a [`MessageDialogResult`] enum that indicates which button was pressed. + pub fn show_with_result(self, f: F) { + show_message_dialog(self, f) + } + + /// Shows a message dialog. + /// + /// Returns `true` if the user pressed the OK/Yes button, + /// + /// This is a blocking operation, + /// and should *NOT* be used when running on the main thread context. + pub fn blocking_show(self) -> bool { + blocking_fn!(self, show) + } + + /// Shows a message dialog and returns the button that was pressed. + /// + /// Returns a [`MessageDialogResult`] enum that indicates which button was pressed. + /// + /// This is a blocking operation, + /// and should *NOT* be used when running on the main thread context. + pub fn blocking_show_with_result(self) -> MessageDialogResult { + blocking_fn!(self, show_with_result) + } +} +#[derive(Debug, Serialize)] +pub(crate) struct Filter { + pub name: String, + pub extensions: Vec, +} + +/// The file dialog builder. +/// +/// Constructs file picker dialogs that can select single/multiple files or directories. +#[derive(Debug)] +pub struct FileDialogBuilder { + #[allow(dead_code)] + pub(crate) dialog: Dialog, + pub(crate) filters: Vec, + pub(crate) starting_directory: Option, + pub(crate) file_name: Option, + pub(crate) title: Option, + pub(crate) can_create_directories: Option, + pub(crate) picker_mode: Option, + pub(crate) file_access_mode: Option, + #[cfg(desktop)] + pub(crate) parent: Option, +} + +#[cfg(mobile)] +#[derive(Serialize)] +#[serde(rename_all = "camelCase")] +pub(crate) struct FileDialogPayload<'a> { + file_name: &'a Option, + filters: &'a Vec, + multiple: bool, + picker_mode: &'a Option, + file_access_mode: &'a Option, +} + +// raw window handle :( +unsafe impl Send for FileDialogBuilder {} + +impl FileDialogBuilder { + /// Gets the default file dialog builder. + pub fn new(dialog: Dialog) -> Self { + Self { + dialog, + filters: Vec::new(), + starting_directory: None, + file_name: None, + title: None, + can_create_directories: None, + picker_mode: None, + file_access_mode: None, + #[cfg(desktop)] + parent: None, + } + } + + #[cfg(mobile)] + pub(crate) fn payload(&self, multiple: bool) -> FileDialogPayload<'_> { + FileDialogPayload { + file_name: &self.file_name, + filters: &self.filters, + multiple, + picker_mode: &self.picker_mode, + file_access_mode: &self.file_access_mode, + } + } + + /// Add file extension filter. Takes in the name of the filter, and list of extensions + #[must_use] + pub fn add_filter(mut self, name: impl Into, extensions: &[&str]) -> Self { + self.filters.push(Filter { + name: name.into(), + extensions: extensions.iter().map(|e| e.to_string()).collect(), + }); + self + } + + /// Set starting directory of the dialog. + #[must_use] + pub fn set_directory>(mut self, directory: P) -> Self { + self.starting_directory.replace(directory.as_ref().into()); + self + } + + /// Set starting file name of the dialog. + #[must_use] + pub fn set_file_name(mut self, file_name: impl Into) -> Self { + self.file_name.replace(file_name.into()); + self + } + + /// Sets the parent window of the dialog. + #[cfg(desktop)] + #[must_use] + pub fn set_parent< + W: raw_window_handle::HasWindowHandle + raw_window_handle::HasDisplayHandle, + >( + mut self, + parent: &W, + ) -> Self { + if let (Ok(window_handle), Ok(display_handle)) = + (parent.window_handle(), parent.display_handle()) + { + self.parent.replace(crate::desktop::WindowHandle::new( + window_handle.as_raw(), + display_handle.as_raw(), + )); + } + self + } + + /// Set the title of the dialog. + #[must_use] + pub fn set_title(mut self, title: impl Into) -> Self { + self.title.replace(title.into()); + self + } + + /// Set whether it should be possible to create new directories in the dialog. Enabled by default. **macOS only**. + pub fn set_can_create_directories(mut self, can: bool) -> Self { + self.can_create_directories.replace(can); + self + } + + /// Set the picker mode of the dialog. + /// This is meant for mobile platforms (iOS and Android) which have distinct file and media pickers. + /// On desktop, this option is ignored. + /// If not provided, the dialog will automatically choose the best mode based on the MIME types of the filters. + pub fn set_picker_mode(mut self, mode: PickerMode) -> Self { + self.picker_mode.replace(mode); + self + } + + /// Set the file access mode of the dialog. + /// This is only used on iOS. + /// On desktop and Android, this option is ignored. + pub fn set_file_access_mode(mut self, mode: FileAccessMode) -> Self { + self.file_access_mode.replace(mode); + self + } + + /// Shows the dialog to select a single file. + /// + /// This is not a blocking operation, + /// and should be used when running on the main thread to avoid deadlocks with the event loop. + /// + /// See [`Self::blocking_pick_file`] for a blocking version for use in other contexts. + /// + /// # Examples + /// + /// ```no_run + /// use tauri_plugin_dialog::DialogExt; + /// tauri::Builder::default() + /// .setup(|app| { + /// app.dialog().file().pick_file(|file_path| { + /// // do something with the optional file path here + /// // the file path is `None` if the user closed the dialog + /// }); + /// Ok(()) + /// }); + /// ``` + pub fn pick_file) + Send + 'static>(self, f: F) { + pick_file(self, f) + } + + /// Shows the dialog to select multiple files. + /// + /// This is not a blocking operation, + /// and should be used when running on the main thread to avoid deadlocks with the event loop. + /// + /// See [`Self::blocking_pick_files`] for a blocking version for use in other contexts. + /// + /// # Reading the files + /// + /// The file paths cannot be read directly on Android as they are behind a content URI. + /// The recommended way to read the files is using the [`fs`](https://v2.tauri.app/plugin/file-system/) plugin: + /// + /// ```no_run + /// use tauri_plugin_dialog::DialogExt; + /// use tauri_plugin_fs::FsExt; + /// tauri::Builder::default() + /// .setup(|app| { + /// let handle = app.handle().clone(); + /// app.dialog().file().pick_file(move |file_path| { + /// let Some(path) = file_path else { return }; + /// let Ok(contents) = handle.fs().read_to_string(path) else { + /// eprintln!("failed to read file, "); + /// return; + /// }; + /// }); + /// Ok(()) + /// }); + /// ``` + /// + /// See for more information. + /// + /// # Examples + /// + /// ```no_run + /// use tauri_plugin_dialog::DialogExt; + /// tauri::Builder::default() + /// .setup(|app| { + /// app.dialog().file().pick_files(|file_paths| { + /// // do something with the optional file paths here + /// // the file paths value is `None` if the user closed the dialog + /// }); + /// Ok(()) + /// }); + /// ``` + pub fn pick_files>) + Send + 'static>(self, f: F) { + pick_files(self, f) + } + + /// Shows the dialog to select a single folder. + /// + /// This is not a blocking operation, + /// and should be used when running on the main thread to avoid deadlocks with the event loop. + /// + /// See [`Self::blocking_pick_folder`] for a blocking version for use in other contexts. + /// + /// # Examples + /// + /// ```no_run + /// use tauri_plugin_dialog::DialogExt; + /// tauri::Builder::default() + /// .setup(|app| { + /// app.dialog().file().pick_folder(|folder_path| { + /// // do something with the optional folder path here + /// // the folder path is `None` if the user closed the dialog + /// }); + /// Ok(()) + /// }); + /// ``` + #[cfg(desktop)] + pub fn pick_folder) + Send + 'static>(self, f: F) { + pick_folder(self, f) + } + + /// Shows the dialog to select multiple folders. + /// + /// This is not a blocking operation, + /// and should be used when running on the main thread to avoid deadlocks with the event loop. + /// + /// See [`Self::blocking_pick_folders`] for a blocking version for use in other contexts. + /// + /// # Examples + /// + /// ```no_run + /// use tauri_plugin_dialog::DialogExt; + /// tauri::Builder::default() + /// .setup(|app| { + /// app.dialog().file().pick_folders(|file_paths| { + /// // do something with the optional folder paths here + /// // the folder paths value is `None` if the user closed the dialog + /// }); + /// Ok(()) + /// }); + /// ``` + #[cfg(desktop)] + pub fn pick_folders>) + Send + 'static>(self, f: F) { + pick_folders(self, f) + } + + /// Shows the dialog to save a file. + /// + /// This is not a blocking operation, + /// and should be used when running on the main thread to avoid deadlocks with the event loop. + /// + /// See [`Self::blocking_save_file`] for a blocking version for use in other contexts. + /// + /// # Examples + /// + /// ```no_run + /// use tauri_plugin_dialog::DialogExt; + /// tauri::Builder::default() + /// .setup(|app| { + /// app.dialog().file().save_file(|file_path| { + /// // do something with the optional file path here + /// // the file path is `None` if the user closed the dialog + /// }); + /// Ok(()) + /// }); + /// ``` + pub fn save_file) + Send + 'static>(self, f: F) { + save_file(self, f) + } +} + +/// Blocking APIs. +impl FileDialogBuilder { + /// Shows the dialog to select a single file. + /// + /// This is a blocking operation, + /// and should *NOT* be used when running on the main thread. + /// + /// See [`Self::pick_file`] for a non-blocking version for use in main-thread contexts. + /// + /// # Examples + /// + /// ```no_run + /// use tauri_plugin_dialog::DialogExt; + /// #[tauri::command] + /// async fn my_command(app: tauri::AppHandle) { + /// let file_path = app.dialog().file().blocking_pick_file(); + /// // do something with the optional file path here + /// // the file path is `None` if the user closed the dialog + /// } + /// ``` + pub fn blocking_pick_file(self) -> Option { + blocking_fn!(self, pick_file) + } + + /// Shows the dialog to select multiple files. + /// + /// This is a blocking operation, + /// and should *NOT* be used when running on the main thread. + /// + /// See [`Self::pick_files`] for a non-blocking version for use in main-thread contexts. + /// + /// # Examples + /// + /// ```no_run + /// use tauri_plugin_dialog::DialogExt; + /// #[tauri::command] + /// async fn my_command(app: tauri::AppHandle) { + /// let file_path = app.dialog().file().blocking_pick_files(); + /// // do something with the optional file paths here + /// // the file paths value is `None` if the user closed the dialog + /// } + /// ``` + pub fn blocking_pick_files(self) -> Option> { + blocking_fn!(self, pick_files) + } + + /// Shows the dialog to select a single folder. + /// + /// This is a blocking operation, + /// and should *NOT* be used when running on the main thread. + /// + /// See [`Self::pick_folder`] for a non-blocking version for use in main-thread contexts. + /// + /// # Examples + /// + /// ```no_run + /// use tauri_plugin_dialog::DialogExt; + /// #[tauri::command] + /// async fn my_command(app: tauri::AppHandle) { + /// let folder_path = app.dialog().file().blocking_pick_folder(); + /// // do something with the optional folder path here + /// // the folder path is `None` if the user closed the dialog + /// } + /// ``` + #[cfg(desktop)] + pub fn blocking_pick_folder(self) -> Option { + blocking_fn!(self, pick_folder) + } + + /// Shows the dialog to select multiple folders. + /// + /// This is a blocking operation, + /// and should *NOT* be used when running on the main thread. + /// + /// See [`Self::pick_folders`] for a non-blocking version for use in main-thread contexts. + /// + /// # Examples + /// + /// ```no_run + /// use tauri_plugin_dialog::DialogExt; + /// #[tauri::command] + /// async fn my_command(app: tauri::AppHandle) { + /// let folder_paths = app.dialog().file().blocking_pick_folders(); + /// // do something with the optional folder paths here + /// // the folder paths value is `None` if the user closed the dialog + /// } + /// ``` + #[cfg(desktop)] + pub fn blocking_pick_folders(self) -> Option> { + blocking_fn!(self, pick_folders) + } + + /// Shows the dialog to save a file. + /// + /// This is a blocking operation, + /// and should *NOT* be used when running on the main thread. + /// + /// See [`Self::save_file`] for a non-blocking version for use in main-thread contexts. + /// + /// # Examples + /// + /// ```no_run + /// use tauri_plugin_dialog::DialogExt; + /// #[tauri::command] + /// async fn my_command(app: tauri::AppHandle) { + /// let file_path = app.dialog().file().blocking_save_file(); + /// // do something with the optional file path here + /// // the file path is `None` if the user closed the dialog + /// } + /// ``` + pub fn blocking_save_file(self) -> Option { + blocking_fn!(self, save_file) + } +} diff --git a/plugins/dialog/src/mobile.rs b/plugins/dialog/src/mobile.rs new file mode 100644 index 0000000..46ea3a2 --- /dev/null +++ b/plugins/dialog/src/mobile.rs @@ -0,0 +1,127 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use serde::{de::DeserializeOwned, Deserialize}; +use tauri::{ + plugin::{PluginApi, PluginHandle}, + AppHandle, Runtime, +}; + +use crate::{FileDialogBuilder, FilePath, MessageDialogBuilder, MessageDialogResult}; + +#[cfg(target_os = "android")] +const PLUGIN_IDENTIFIER: &str = "app.tauri.dialog"; + +#[cfg(target_os = "ios")] +tauri::ios_plugin_binding!(init_plugin_dialog); + +// initializes the Kotlin or Swift plugin classes +pub fn init( + _app: &AppHandle, + api: PluginApi, +) -> crate::Result> { + #[cfg(target_os = "android")] + let handle = api.register_android_plugin(PLUGIN_IDENTIFIER, "DialogPlugin")?; + #[cfg(target_os = "ios")] + let handle = api.register_ios_plugin(init_plugin_dialog)?; + Ok(Dialog(handle)) +} + +/// Access to the dialog APIs. +#[derive(Debug)] +pub struct Dialog(PluginHandle); + +impl Clone for Dialog { + fn clone(&self) -> Self { + Self(self.0.clone()) + } +} + +impl Dialog { + pub(crate) fn app_handle(&self) -> &AppHandle { + self.0.app() + } +} + +#[derive(Debug, Deserialize)] +struct FilePickerResponse { + files: Vec, +} + +#[derive(Debug, Deserialize)] +struct SaveFileResponse { + file: FilePath, +} + +pub fn pick_file) + Send + 'static>( + dialog: FileDialogBuilder, + f: F, +) { + std::thread::spawn(move || { + let res = dialog + .dialog + .0 + .run_mobile_plugin::("showFilePicker", dialog.payload(false)); + if let Ok(response) = res { + f(Some(response.files.into_iter().next().unwrap())) + } else { + f(None) + } + }); +} + +pub fn pick_files>) + Send + 'static>( + dialog: FileDialogBuilder, + f: F, +) { + std::thread::spawn(move || { + let res = dialog + .dialog + .0 + .run_mobile_plugin::("showFilePicker", dialog.payload(true)); + if let Ok(response) = res { + f(Some(response.files)) + } else { + f(None) + } + }); +} + +pub fn save_file) + Send + 'static>( + dialog: FileDialogBuilder, + f: F, +) { + std::thread::spawn(move || { + let res = dialog + .dialog + .0 + .run_mobile_plugin::("saveFileDialog", dialog.payload(false)); + if let Ok(response) = res { + f(Some(response.file)) + } else { + f(None) + } + }); +} + +#[derive(Debug, Deserialize)] +struct ShowMessageDialogResponse { + value: String, +} + +/// Shows a message dialog +pub fn show_message_dialog( + dialog: MessageDialogBuilder, + f: F, +) { + std::thread::spawn(move || { + let res = dialog + .dialog + .0 + .run_mobile_plugin::("showMessageDialog", dialog.payload()); + + let res = res.map(|res| res.value.into()); + f(res.unwrap_or_default()) + }); +} diff --git a/plugins/dialog/src/models.rs b/plugins/dialog/src/models.rs new file mode 100644 index 0000000..2c4f6ec --- /dev/null +++ b/plugins/dialog/src/models.rs @@ -0,0 +1,109 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use serde::{Deserialize, Deserializer, Serialize, Serializer}; + +/// Types of message, ask and confirm dialogs. +#[non_exhaustive] +#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Default)] +pub enum MessageDialogKind { + /// Information dialog. + #[default] + Info, + /// Warning dialog. + Warning, + /// Error dialog. + Error, +} + +impl<'de> Deserialize<'de> for MessageDialogKind { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let s = String::deserialize(deserializer)?; + Ok(match s.to_lowercase().as_str() { + "info" => MessageDialogKind::Info, + "warning" => MessageDialogKind::Warning, + "error" => MessageDialogKind::Error, + _ => MessageDialogKind::Info, + }) + } +} + +impl Serialize for MessageDialogKind { + fn serialize(&self, serializer: S) -> std::result::Result + where + S: Serializer, + { + match self { + Self::Info => serializer.serialize_str("info"), + Self::Warning => serializer.serialize_str("warning"), + Self::Error => serializer.serialize_str("error"), + } + } +} + +/// Set of button that will be displayed on the dialog +#[non_exhaustive] +#[derive(Debug, Default, Clone, Serialize, Deserialize)] +pub enum MessageDialogButtons { + #[default] + /// A single `Ok` button with OS default dialog text + Ok, + /// 2 buttons `Ok` and `Cancel` with OS default dialog texts + OkCancel, + /// 2 buttons `Yes` and `No` with OS default dialog texts + YesNo, + /// 3 buttons `Yes`, `No` and `Cancel` with OS default dialog texts + YesNoCancel, + /// A single `Ok` button with custom text + OkCustom(String), + /// 2 buttons `Ok` and `Cancel` with custom texts + OkCancelCustom(String, String), + /// 3 buttons `Yes`, `No` and `Cancel` with custom texts + YesNoCancelCustom(String, String, String), +} + +/// Result of a message dialog +#[derive(Debug, Default, Clone, PartialEq, Eq, Deserialize, Serialize)] +pub enum MessageDialogResult { + /// The user pressed the `Yes` button. + Yes, + /// The user pressed the `No` button. + No, + /// The user pressed the `Ok` button. + Ok, + /// The user pressed the `Cancel` button, or closed the dialog without pressing a button. + #[default] + Cancel, + /// The user pressed a button with a custom label, holding that label's text. + #[serde(untagged)] + Custom(String), +} + +#[cfg(desktop)] +impl From for MessageDialogResult { + fn from(result: rfd::MessageDialogResult) -> Self { + match result { + rfd::MessageDialogResult::Yes => Self::Yes, + rfd::MessageDialogResult::No => Self::No, + rfd::MessageDialogResult::Ok => Self::Ok, + rfd::MessageDialogResult::Cancel => Self::Cancel, + rfd::MessageDialogResult::Custom(s) => Self::Custom(s), + } + } +} + +impl From for MessageDialogResult { + fn from(value: String) -> Self { + match value.as_str() { + "Yes" => Self::Yes, + "No" => Self::No, + "Ok" => Self::Ok, + "Cancel" => Self::Cancel, + _ => Self::Custom(value), + } + } +} diff --git a/plugins/dialog/test/tauri.conf.json b/plugins/dialog/test/tauri.conf.json new file mode 100644 index 0000000..4d0ae02 --- /dev/null +++ b/plugins/dialog/test/tauri.conf.json @@ -0,0 +1,21 @@ +{ + "identifier": "app.tauri.example", + "build": { + "frontendDist": ".", + "devUrl": "http://localhost:4000" + }, + "app": { + "windows": [ + { + "title": "Tauri App" + } + ], + "security": { + "csp": "default-src blob: data: filesystem: ws: wss: http: https: tauri: http://tauri.localhost 'unsafe-eval' 'unsafe-inline' 'self'" + } + }, + "bundle": { + "active": true, + "icon": ["../../../examples/api/src-tauri/icons/icon.png"] + } +} diff --git a/plugins/dialog/tsconfig.json b/plugins/dialog/tsconfig.json new file mode 100644 index 0000000..4374468 --- /dev/null +++ b/plugins/dialog/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "rootDir": "guest-js" + }, + "include": ["guest-js/*.ts"] +} diff --git a/plugins/fs/CHANGELOG.md b/plugins/fs/CHANGELOG.md new file mode 100644 index 0000000..2622d13 --- /dev/null +++ b/plugins/fs/CHANGELOG.md @@ -0,0 +1,232 @@ +# Changelog + +## [2.5.2] + +- [`db9c5998`](https://github.com/tauri-apps/plugins-workspace/commit/db9c5998feff9384f9cbbefcbe0d45937c00a1fc) ([#3531](https://github.com/tauri-apps/plugins-workspace/pull/3531) by [@Legend-Master](https://github.com/tauri-apps/plugins-workspace/../../Legend-Master)) Fix missing `consumer-rules.pro` on Gradle v9. +- [`f8053e65`](https://github.com/tauri-apps/plugins-workspace/commit/f8053e659e4ccd85c1f52833411ff8417cbc5e69) ([#3527](https://github.com/tauri-apps/plugins-workspace/pull/3527) by [@Legend-Master](https://github.com/tauri-apps/plugins-workspace/../../Legend-Master)) Documented Cargo feature flags in each plugin's crate-level documentation. +- [`685610ae`](https://github.com/tauri-apps/plugins-workspace/commit/685610ae782b668770d412f42a5d52d2cc9e497e) ([#3507](https://github.com/tauri-apps/plugins-workspace/pull/3507) by [@Legend-Master](https://github.com/tauri-apps/plugins-workspace/../../Legend-Master)) Fix `fs:default` and `fs:read-app-specific-dirs-recursive` not giving any command scopes +- [`3fb27bf1`](https://github.com/tauri-apps/plugins-workspace/commit/3fb27bf13a88dd1e5aa2cc933408be02b0eafc91) ([#3504](https://github.com/tauri-apps/plugins-workspace/pull/3504) by [@vasfvitor](https://github.com/tauri-apps/plugins-workspace/../../vasfvitor)) Fixed `deny-webview-data` has no effect, and on Linux, only deny access to the webview data paths instead of the entire `$APPLOCALDATA` + +## \[2.5.1] + +- [`ec054013`](https://github.com/tauri-apps/plugins-workspace/commit/ec0540138bece081e9a87982091947360e61987a) ([#3323](https://github.com/tauri-apps/plugins-workspace/pull/3323) by [@renovate](https://github.com/tauri-apps/plugins-workspace/../../renovate)) Updated dependency `toml` from 0.9 to 1 + +## \[2.5.0] + +- [`e97a4ded`](https://github.com/tauri-apps/plugins-workspace/commit/e97a4dedab8608d9d57029784ee8a2607ca3a996) ([#3244](https://github.com/tauri-apps/plugins-workspace/pull/3244) by [@SeijiOkuda](https://github.com/tauri-apps/plugins-workspace/../../SeijiOkuda)) Add `encoding` option for `readTextFile` and `readTextFileLines` +- [`f75d21db`](https://github.com/tauri-apps/plugins-workspace/commit/f75d21db3351d6f12adf585c2c797c20ece94f7f) ([#3360](https://github.com/tauri-apps/plugins-workspace/pull/3360) by [@Legend-Master](https://github.com/tauri-apps/plugins-workspace/../../Legend-Master)) Removed the dependency on `tauri-utils`'s `build` feature +- [`f5f68063`](https://github.com/tauri-apps/plugins-workspace/commit/f5f68063e459c33522f44a04df3120d9d039ec13) ([#3185](https://github.com/tauri-apps/plugins-workspace/pull/3185) by [@lucasfernog](https://github.com/tauri-apps/plugins-workspace/../../lucasfernog)) Enable access for security-scoped resources on iOS by automatically calling `NSURL::startAccessingSecurityScopedResource` on resource access and adding the `stopAccessingSecurityScopedResource` API. + +## \[2.4.5] + +- [`521cd8b3`](https://github.com/tauri-apps/plugins-workspace/commit/521cd8b372c862d96b1637775710e4d7cf2443e2) ([#3155](https://github.com/tauri-apps/plugins-workspace/pull/3155) by [@EliasStar](https://github.com/tauri-apps/plugins-workspace/../../EliasStar)) Fix off by one error in the implementation of readTextFileLines causing all lines to end with an (additional) null byte. + Issue: [#3154](https://github.com/tauri-apps/plugins-workspace/issues/3154) + PR: [#3155](https://github.com/tauri-apps/plugins-workspace/pull/3155) + +## \[2.4.4] + +- [`93426f85`](https://github.com/tauri-apps/plugins-workspace/commit/93426f85120f49beb9f40222bff45185a32d54a9) Fixed an issue that caused docs.rs builds to fail. No user facing changes. + +## \[2.4.3] + +- [`6c9b61fb`](https://github.com/tauri-apps/plugins-workspace/commit/6c9b61fb658145d13893626112fc489f7458aa17) ([#3039](https://github.com/tauri-apps/plugins-workspace/pull/3039) by [@FabianLars](https://github.com/tauri-apps/plugins-workspace/../../FabianLars)) On Android, updated compileSdk to 36. +- [`6b5b1053`](https://github.com/tauri-apps/plugins-workspace/commit/6b5b1053ba8aeb789dd5cb5fb05b7e98f3b8de0b) ([#1939](https://github.com/tauri-apps/plugins-workspace/pull/1939) by [@amrbashir](https://github.com/tauri-apps/plugins-workspace/../../amrbashir)) Enhance error messages. + +## \[2.4.2] + +- [`4eb36b0f`](https://github.com/tauri-apps/plugins-workspace/commit/4eb36b0ff57acb0bb1b911c583efa3bf2f56aa32) ([#2907](https://github.com/tauri-apps/plugins-workspace/pull/2907) by [@Legend-Master](https://github.com/tauri-apps/plugins-workspace/../../Legend-Master)) Fixed calling `writeFile` with `data: ReadableStream` throws `Invalid argument` +- [`515182a1`](https://github.com/tauri-apps/plugins-workspace/commit/515182a179d4439079b2b7f6927555ba5ab0b035) ([#2915](https://github.com/tauri-apps/plugins-workspace/pull/2915) by [@samhinshaw](https://github.com/tauri-apps/plugins-workspace/../../samhinshaw)) `readFile` now returns a more specific type `Promise>` instead of the default `Promise` + +## \[2.4.1] + +- [`44a1f659`](https://github.com/tauri-apps/plugins-workspace/commit/44a1f659125a341191420e650608b0b6ff316a0e) ([#2846](https://github.com/tauri-apps/plugins-workspace/pull/2846) by [@Legend-Master](https://github.com/tauri-apps/plugins-workspace/../../Legend-Master)) Fix `writeFile` doesn't create a new file by default when the data is a `ReadableStream` + +## \[2.4.0] + +- [`f209b2f2`](https://github.com/tauri-apps/plugins-workspace/commit/f209b2f23cb29133c97ad5961fb46ef794dbe063) ([#2804](https://github.com/tauri-apps/plugins-workspace/pull/2804) by [@renovate](https://github.com/tauri-apps/plugins-workspace/../../renovate)) Updated tauri to 2.6 + +## \[2.3.0] + +- [`dac4d537`](https://github.com/tauri-apps/plugins-workspace/commit/dac4d53724bb3430a00a3f0119857cba32a031e8) ([#2613](https://github.com/tauri-apps/plugins-workspace/pull/2613) by [@Legend-Master](https://github.com/tauri-apps/plugins-workspace/../../Legend-Master)) Reduce the overhead of `watch` and `unwatch` + +## \[2.2.1] + +### bug + +- [`831c35ff`](https://github.com/tauri-apps/plugins-workspace/commit/831c35ff3940e841fe4418bb4cb104038b03304b) ([#2550](https://github.com/tauri-apps/plugins-workspace/pull/2550)) Fix `writeFile` ReadableStream handling due to missing async iterator support on macOS platform + +## \[2.2.0] + +- [`3a79266b`](https://github.com/tauri-apps/plugins-workspace/commit/3a79266b8cf96a55b1ae6339d725567d45a44b1d) ([#2173](https://github.com/tauri-apps/plugins-workspace/pull/2173) by [@FabianLars](https://github.com/tauri-apps/plugins-workspace/../../FabianLars)) Bumped all plugins to `v2.2.0`. From now, the versions for the Rust and JavaScript packages of each plugin will be in sync with each other. + +## \[2.0.4] + +- [`77b85507`](https://github.com/tauri-apps/plugins-workspace/commit/77b855074aad612f2b28e6a3b5881fac767a05ae) ([#2171](https://github.com/tauri-apps/plugins-workspace/pull/2171) by [@FabianLars](https://github.com/tauri-apps/plugins-workspace/../../FabianLars)) Fixed docs.rs build. + +## \[2.0.3] + +- [`ed981027`](https://github.com/tauri-apps/plugins-workspace/commit/ed981027dd4fba7d0e2f836eb5db34d344388d73) ([#1962](https://github.com/tauri-apps/plugins-workspace/pull/1962) by [@amrbashir](https://github.com/tauri-apps/plugins-workspace/../../amrbashir)) Improve performance of `readTextFile` and `readTextFileLines` APIs +- [`3e78173d`](https://github.com/tauri-apps/plugins-workspace/commit/3e78173df9ce90aa3b19e1f36d1f8712c5020fb6) ([#2018](https://github.com/tauri-apps/plugins-workspace/pull/2018) by [@amrbashir](https://github.com/tauri-apps/plugins-workspace/../../amrbashir)) Fix `readDir` function failing to read directories that contain broken symlinks. +- [`5092ea5e`](https://github.com/tauri-apps/plugins-workspace/commit/5092ea5e89817c0550d09b0a4ad17bf1253b23df) ([#1964](https://github.com/tauri-apps/plugins-workspace/pull/1964) by [@amrbashir](https://github.com/tauri-apps/plugins-workspace/../../amrbashir)) Add support for using `ReadableStream` with `writeFile` API. + +## \[2.0.2] + +- [`77149dc4`](https://github.com/tauri-apps/plugins-workspace/commit/77149dc4320d26b413e4a6bbe82c654367c51b32) ([#1965](https://github.com/tauri-apps/plugins-workspace/pull/1965) by [@amrbashir](https://github.com/tauri-apps/plugins-workspace/../../amrbashir)) Fix `writeTextFile` converting UTF-8 characters (for example `äöü`) in the given path into replacement character (`�`) + +## \[2.0.3] + +- [`14cee64c`](https://github.com/tauri-apps/plugins-workspace/commit/14cee64c82a72655ae6a4ac0892736a2959dbda5) ([#1958](https://github.com/tauri-apps/plugins-workspace/pull/1958) by [@bWanShiTong](https://github.com/tauri-apps/plugins-workspace/../../bWanShiTong)) Fix compilation on targets with pointer width of `16` or `32` + +## \[2.0.1] + +- [`ae802456`](https://github.com/tauri-apps/plugins-workspace/commit/ae8024565f074f313084777c8b10d1b5e3bbe220) ([#1950](https://github.com/tauri-apps/plugins-workspace/pull/1950) by [@amrbashir](https://github.com/tauri-apps/plugins-workspace/../../amrbashir)) Improve performance of the `FileHandle.read` and `writeTextFile` APIs. + +## \[2.0.1] + +- [`a1a82208`](https://github.com/tauri-apps/plugins-workspace/commit/a1a82208ed4ab87f83310be0dc95428aec9ab241) ([#1873](https://github.com/tauri-apps/plugins-workspace/pull/1873) by [@lucasfernog](https://github.com/tauri-apps/plugins-workspace/../../lucasfernog)) Downgrade MSRV to 1.77.2 to support Windows 7. + +## \[2.0.0] + +- [`e2c4dfb6`](https://github.com/tauri-apps/plugins-workspace/commit/e2c4dfb6af43e5dd8d9ceba232c315f5febd55c1) Update to tauri v2 stable release. + +## \[2.0.0-rc.6] + +- [`fc9b189e`](https://github.com/tauri-apps/plugins-workspace/commit/fc9b189e83a29bd750714ec6336133c6eabdfa20) ([#1837](https://github.com/tauri-apps/plugins-workspace/pull/1837) by [@FabianLars](https://github.com/tauri-apps/plugins-workspace/../../FabianLars)) Fix failing to deserialize capability file when using an OS specific path in the scope that is not available on the current OS. + +## \[2.0.0-rc.5] + +- [`cc03ccf5`](https://github.com/tauri-apps/plugins-workspace/commit/cc03ccf5e0e4be8bbf50bbdebe957c84be7f779b) ([#1774](https://github.com/tauri-apps/plugins-workspace/pull/1774)) Fix `scope-app`, `scope-app-recursive` and `scope-index` not properly enabling the application paths. + +## \[2.0.0-rc.4] + +- [`9291e4d2`](https://github.com/tauri-apps/plugins-workspace/commit/9291e4d2caa31c883c71e55f2193bd8754d72f03) ([#1640](https://github.com/tauri-apps/plugins-workspace/pull/1640) by [@SRutile](https://github.com/tauri-apps/plugins-workspace/../../SRutile)) Support any UTF-8 character in the writeFile API. + +## \[2.0.0-rc.3] + +- [`a2fe5551`](https://github.com/tauri-apps/plugins-workspace/commit/a2fe55512f908dd11c814ce021d164f01677572a) ([#1727](https://github.com/tauri-apps/plugins-workspace/pull/1727) by [@amrbashir](https://github.com/tauri-apps/plugins-workspace/../../amrbashir)) Add utility methods on `FilePath` and `SafeFilePath` enums which are: + + - `path` + - `simplified` + - `into_path` +- [`a2fe5551`](https://github.com/tauri-apps/plugins-workspace/commit/a2fe55512f908dd11c814ce021d164f01677572a) ([#1727](https://github.com/tauri-apps/plugins-workspace/pull/1727) by [@amrbashir](https://github.com/tauri-apps/plugins-workspace/../../amrbashir)) Implement `Serialize`, `Deserialize`, `From`, `TryFrom` and `FromStr` traits for `FilePath` and `SafeFilePath` enums. +- [`a2fe5551`](https://github.com/tauri-apps/plugins-workspace/commit/a2fe55512f908dd11c814ce021d164f01677572a) ([#1727](https://github.com/tauri-apps/plugins-workspace/pull/1727) by [@amrbashir](https://github.com/tauri-apps/plugins-workspace/../../amrbashir)) Mark `Error` enum as `#[non_exhuastive]`. +- [`a2fe5551`](https://github.com/tauri-apps/plugins-workspace/commit/a2fe55512f908dd11c814ce021d164f01677572a) ([#1727](https://github.com/tauri-apps/plugins-workspace/pull/1727) by [@amrbashir](https://github.com/tauri-apps/plugins-workspace/../../amrbashir)) Add `SafeFilePath` enum. + +## \[2.0.0-rc.2] + +- [`f7280c88`](https://github.com/tauri-apps/plugins-workspace/commit/f7280c88309cdf1f2330574fec31e26e01e9cdbd) ([#1710](https://github.com/tauri-apps/plugins-workspace/pull/1710) by [@Legend-Master](https://github.com/tauri-apps/plugins-workspace/../../Legend-Master)) Fix can't use Windows paths like `C:/Users/UserName/file.txt` + +### bug + +- [`51819c60`](https://github.com/tauri-apps/plugins-workspace/commit/51819c601f863cbfbd11809a1c4cf9df5a20b1e0) ([#1708](https://github.com/tauri-apps/plugins-workspace/pull/1708) by [@lucasfernog](https://github.com/tauri-apps/plugins-workspace/../../lucasfernog)) Fixes `writeFile` command implementation on Android. + +## \[2.0.0-rc.2] + +- [`e2e97db5`](https://github.com/tauri-apps/plugins-workspace/commit/e2e97db51983267f5be84d4f6f0278d58834d1f5) ([#1701](https://github.com/tauri-apps/plugins-workspace/pull/1701) by [@lucasfernog](https://github.com/tauri-apps/plugins-workspace/../../lucasfernog)) Update to tauri 2.0.0-rc.8 + +## \[2.0.0-rc.1] + +- [`5f689902`](https://github.com/tauri-apps/plugins-workspace/commit/5f68990297f2cefac4220649a469adb7fa94fe1b) ([#1645](https://github.com/tauri-apps/plugins-workspace/pull/1645) by [@lucasfernog](https://github.com/tauri-apps/plugins-workspace/../../lucasfernog)) Update documentation. + +## \[2.0.0-rc.0] + +- [`9887d1`](https://github.com/tauri-apps/plugins-workspace/commit/9887d14bd0e971c4c0f5c1188fc4005d3fc2e29e) Update to tauri RC. + +## \[2.0.0-beta.8] + +- [`99d6ac0f`](https://github.com/tauri-apps/plugins-workspace/commit/99d6ac0f9506a6a4a1aa59c728157190a7441af6) ([#1606](https://github.com/tauri-apps/plugins-workspace/pull/1606) by [@FabianLars](https://github.com/tauri-apps/plugins-workspace/../../FabianLars)) The JS packages now specify the *minimum* `@tauri-apps/api` version instead of a single exact version. +- [`6de87966`](https://github.com/tauri-apps/plugins-workspace/commit/6de87966ecc00ad9d91c25be452f1f46bd2b7e1f) ([#1597](https://github.com/tauri-apps/plugins-workspace/pull/1597) by [@Legend-Master](https://github.com/tauri-apps/plugins-workspace/../../Legend-Master)) Update to tauri beta.25. + +## \[2.0.0-beta.7] + +- [`22a17980`](https://github.com/tauri-apps/plugins-workspace/commit/22a17980ff4f6f8c40adb1b8f4ffc6dae2fe7e30) ([#1537](https://github.com/tauri-apps/plugins-workspace/pull/1537) by [@lucasfernog](https://github.com/tauri-apps/plugins-workspace/../../lucasfernog)) Update to tauri beta.24. + +## \[2.0.0-beta.6] + +- [`76daee7a`](https://github.com/tauri-apps/plugins-workspace/commit/76daee7aafece34de3092c86e531cf9eb1138989) ([#1512](https://github.com/tauri-apps/plugins-workspace/pull/1512) by [@renovate](https://github.com/tauri-apps/plugins-workspace/../../renovate)) Update to tauri beta.23. + +## \[2.0.0-beta.5] + +- [`9013854f`](https://github.com/tauri-apps/plugins-workspace/commit/9013854f42a49a230b9dbb9d02774765528a923f)([#1382](https://github.com/tauri-apps/plugins-workspace/pull/1382)) Update to tauri beta.22. + +## \[2.0.0-beta.4] + +- [`430bd6f4`](https://github.com/tauri-apps/plugins-workspace/commit/430bd6f4f379bee5d232ae6b098ae131db7f178a)([#1363](https://github.com/tauri-apps/plugins-workspace/pull/1363)) Update to tauri beta.20. + +## \[2.0.0-beta.3] + +- [`bd1ed590`](https://github.com/tauri-apps/plugins-workspace/commit/bd1ed5903ffcce5500310dac1e59e8c67674ef1e)([#1237](https://github.com/tauri-apps/plugins-workspace/pull/1237)) Update to tauri beta.17. + +## \[2.0.0-beta.6] + +- [`b115fd22`](https://github.com/tauri-apps/plugins-workspace/commit/b115fd22e0da073f5d758c13474ec2106cf78163)([#1221](https://github.com/tauri-apps/plugins-workspace/pull/1221)) Fixes an issue that caused the app to freeze when the `dialog`, `fs`, and `persisted-scope` plugins were used together. + +## \[2.0.0-beta.5] + +- [`bb51a41`](https://github.com/tauri-apps/plugins-workspace/commit/bb51a41d67ebf989e8aedf10c4b1a7f9514d1bdf)([#1168](https://github.com/tauri-apps/plugins-workspace/pull/1168)) **Breaking Change:** All apis that return paths to the frontend will now remove the `\\?\` UNC prefix on Windows. +- [`e3d41f4`](https://github.com/tauri-apps/plugins-workspace/commit/e3d41f4011bd3ea3ce281bb38bbe31d3709f8e0f)([#1191](https://github.com/tauri-apps/plugins-workspace/pull/1191)) Internally use the webview scoped resources table instead of the app one, so other webviews can't access other webviews resources. +- [`e3d41f4`](https://github.com/tauri-apps/plugins-workspace/commit/e3d41f4011bd3ea3ce281bb38bbe31d3709f8e0f)([#1191](https://github.com/tauri-apps/plugins-workspace/pull/1191)) Update for tauri 2.0.0-beta.15. + +## \[2.0.0-beta.4] + +- [`9c2fb93`](https://github.com/tauri-apps/plugins-workspace/commit/9c2fb9306ecd3936a2aef56b3c012899036db098) Enhance the scope type to also allow a plain string representing the path to allow or deny. +- [`772f2bc`](https://github.com/tauri-apps/plugins-workspace/commit/772f2bc3495a4f83f1c3e538cbac6d29cbd7d5ef)([#1136](https://github.com/tauri-apps/plugins-workspace/pull/1136)) Update for tauri 2.0.0-beta.14. + +## \[2.0.0-beta.3] + +- [`cb96aa0`](https://github.com/tauri-apps/plugins-workspace/commit/cb96aa06277f7b864952827ec9fb1e74c8a1f761)([#1082](https://github.com/tauri-apps/plugins-workspace/pull/1082)) Fixes `watch` and `watchImmediate` which previously ignored the `baseDir` parameter. +- [`a04ea2f`](https://github.com/tauri-apps/plugins-workspace/commit/a04ea2f38294d5a3987578283badc8eec87a7752)([#1071](https://github.com/tauri-apps/plugins-workspace/pull/1071)) The global API script is now only added to the binary when the `withGlobalTauri` config is true. + +## \[2.0.0-beta.2] + +- [`7358102`](https://github.com/tauri-apps/plugins-workspace/commit/735810237e21504a027a65a7b3c25fd7e594288a)([#1029](https://github.com/tauri-apps/plugins-workspace/pull/1029)) Fix infinite loop on cargo build operations +- [`99bea25`](https://github.com/tauri-apps/plugins-workspace/commit/99bea2559c2c0648c2519c50a18cd124dacef57b)([#1005](https://github.com/tauri-apps/plugins-workspace/pull/1005)) Update to tauri beta.8. + +## \[2.0.0-beta.1] + +- [`569defb`](https://github.com/tauri-apps/plugins-workspace/commit/569defbe9492e38938554bb7bdc1be9151456d21) Update to tauri beta.4. + +## \[2.0.0-beta.0] + +- [`d198c01`](https://github.com/tauri-apps/plugins-workspace/commit/d198c014863ee260cb0de88a14b7fc4356ef7474)([#862](https://github.com/tauri-apps/plugins-workspace/pull/862)) Update to tauri beta. +- [`ea8eadc`](https://github.com/tauri-apps/plugins-workspace/commit/ea8eadce85b2e3e8eb7eb1a779fc3aa6c1201fa3)([#865](https://github.com/tauri-apps/plugins-workspace/pull/865)) Fix incorrect `create` option default value for `writeFile` and `writeTextFile` which didn't match documentation. +- [`61edbbe`](https://github.com/tauri-apps/plugins-workspace/commit/61edbbec0acda4213ed8684f75a973e8be123a52)([#885](https://github.com/tauri-apps/plugins-workspace/pull/885)) Replace `notify-debouncer-mini` with `notify-debouncer-full`. [(plugins-workspace#885)](https://github.com/tauri-apps/plugins-workspace/pull/885) + +## \[2.0.0-alpha.6] + +- [`85f8419`](https://github.com/tauri-apps/plugins-workspace/commit/85f841968200316958d707db0c39bb115f762471)([#848](https://github.com/tauri-apps/plugins-workspace/pull/848)) Fix `DebouncedEvent` type to correctly represent the actual type. +- [`c601230`](https://github.com/tauri-apps/plugins-workspace/commit/c60123093ddf725af7228494182fed697ff8b021)([#847](https://github.com/tauri-apps/plugins-workspace/pull/847)) Add `createNew` option for `writeFile` and `writeTextFile` to create the file if doesn't exist and fail if it does. +- [`c601230`](https://github.com/tauri-apps/plugins-workspace/commit/c60123093ddf725af7228494182fed697ff8b021)([#847](https://github.com/tauri-apps/plugins-workspace/pull/847)) Truncate files when using `writeFile` and `writeTextFile` with `append: false`. +- [`2e2fc8d`](https://github.com/tauri-apps/plugins-workspace/commit/2e2fc8de69dd8d282b66ec81561d57d8af802dc5)([#857](https://github.com/tauri-apps/plugins-workspace/pull/857)) Fix `invalid args id for command unwatch` error when trying to unwatch a previously watched file or directory. +- [`c601230`](https://github.com/tauri-apps/plugins-workspace/commit/c60123093ddf725af7228494182fed697ff8b021)([#847](https://github.com/tauri-apps/plugins-workspace/pull/847)) Fix panic when using `writeFile` or `writeTextFile` without passing an option object. + +## \[2.0.0-alpha.5] + +- [`387c2f9`](https://github.com/tauri-apps/plugins-workspace/commit/387c2f9e0ce4c75c07ffa3fd76391a25b58f5daf)([#802](https://github.com/tauri-apps/plugins-workspace/pull/802)) Update to @tauri-apps/api v2.0.0-alpha.13. +- [`69a1fa0`](https://github.com/tauri-apps/plugins-workspace/commit/69a1fa099c3143b6e426492f1c9d9cfbe56d2209)([#751](https://github.com/tauri-apps/plugins-workspace/pull/751)) The `fs` plugin received a major overhaul to add new APIs and changed existing APIs to be closer to Node.js and Deno APIs. + +## \[2.0.0-alpha.4] + +- [`387c2f9`](https://github.com/tauri-apps/plugins-workspace/commit/387c2f9e0ce4c75c07ffa3fd76391a25b58f5daf)([#802](https://github.com/tauri-apps/plugins-workspace/pull/802)) Update to @tauri-apps/api v2.0.0-alpha.12. +- [`88d260d`](https://github.com/tauri-apps/plugins-workspace/commit/88d260d90130f9df4b9ce00c1ad1bf1e4b30b1c0)([#744](https://github.com/tauri-apps/plugins-workspace/pull/744)) Add second argument to `exists` function to specify base directory. + +## \[2.0.0-alpha.3] + +- [`e438e0a`](https://github.com/tauri-apps/plugins-workspace/commit/e438e0a62d4b430a5159f05f13ecd397dd891a0d)([#676](https://github.com/tauri-apps/plugins-workspace/pull/676)) Update to @tauri-apps/api v2.0.0-alpha.11. + +## \[2.0.0-alpha.2] + +- [`5c13736`](https://github.com/tauri-apps/plugins-workspace/commit/5c137365c60790e8d4037d449e8237aa3fffdab0)([#673](https://github.com/tauri-apps/plugins-workspace/pull/673)) Update to @tauri-apps/api v2.0.0-alpha.9. + +## \[2.0.0-alpha.2] + +- [`4e2cef9`](https://github.com/tauri-apps/plugins-workspace/commit/4e2cef9b702bbbb9cf4ee17de50791cb21f1b2a4)([#593](https://github.com/tauri-apps/plugins-workspace/pull/593)) Update to alpha.12. + +## \[2.0.0-alpha.1] + +- [`0bba693`](https://github.com/tauri-apps/plugins-workspace/commit/0bba6932c09da5267a9dbf75ba52252e39458420)([#454](https://github.com/tauri-apps/plugins-workspace/pull/454)) Fix `writeBinaryFile` crashing with `command 'write_binary_file' not found` +- [`d74fc0a`](https://github.com/tauri-apps/plugins-workspace/commit/d74fc0a097996e90a37be8f57d50b7d1f6ca616f)([#555](https://github.com/tauri-apps/plugins-workspace/pull/555)) Update to alpha.11. + +## \[2.0.0-alpha.0] + +- [`717ae67`](https://github.com/tauri-apps/plugins-workspace/commit/717ae670978feb4492fac1f295998b93f2b9347f)([#371](https://github.com/tauri-apps/plugins-workspace/pull/371)) First v2 alpha release! diff --git a/plugins/fs/Cargo.toml b/plugins/fs/Cargo.toml new file mode 100644 index 0000000..74f7f6a --- /dev/null +++ b/plugins/fs/Cargo.toml @@ -0,0 +1,59 @@ +[package] +name = "tauri-plugin-fs" +version = "2.5.2" +description = "Access the file system." +authors = { workspace = true } +license = { workspace = true } +edition = { workspace = true } +rust-version = { workspace = true } +repository = { workspace = true } +links = "tauri-plugin-fs" +exclude = [ + "/banner.png", + "/guest-js", + "/package.json", + "/rollup.config.js", + "/tsconfig.json", +] + +[package.metadata.platforms.support] +windows = { level = "full", notes = "Apps installed via MSI or NSIS in `perMachine` and `both` mode require admin permissions for write access in `$RESOURCES` folder" } +linux = { level = "full", notes = "No write access to `$RESOURCES` folder" } +macos = { level = "full", notes = "No write access to `$RESOURCES` folder" } +android = { level = "partial", notes = "Access is restricted to Application folder by default" } +ios = { level = "partial", notes = "Access is restricted to Application folder by default" } + +[build-dependencies] +tauri-plugin = { workspace = true, features = ["build"] } +schemars = { workspace = true } +serde = { workspace = true } +toml = "1.0" +tauri-utils = { workspace = true } + +[dev-dependencies] +tauri = { workspace = true, features = ["wry"] } + +[dependencies] +serde = { workspace = true } +serde_json = { workspace = true } +serde_repr = "0.1" +tauri = { workspace = true } +thiserror = { workspace = true } +url = { workspace = true } +log = { workspace = true } +anyhow = "1" +glob = { workspace = true } +# TODO: Remove `serialization-compat-6` in v3 +notify = { version = "8", optional = true, features = [ + "serde", + "serialization-compat-6", +] } +notify-debouncer-full = { version = "0.6", optional = true } +dunce = { workspace = true } +percent-encoding = "2" + +[target.'cfg(target_os = "ios")'.dependencies] +objc2-foundation = { version = "0.3", features = ["NSURL", "NSString"] } + +[features] +watch = ["notify", "notify-debouncer-full"] diff --git a/plugins/fs/LICENSE.spdx b/plugins/fs/LICENSE.spdx new file mode 100644 index 0000000..cdd0df5 --- /dev/null +++ b/plugins/fs/LICENSE.spdx @@ -0,0 +1,20 @@ +SPDXVersion: SPDX-2.1 +DataLicense: CC0-1.0 +PackageName: tauri +DataFormat: SPDXRef-1 +PackageSupplier: Organization: The Tauri Programme in the Commons Conservancy +PackageHomePage: https://tauri.app +PackageLicenseDeclared: Apache-2.0 +PackageLicenseDeclared: MIT +PackageCopyrightText: 2019-2022, The Tauri Programme in the Commons Conservancy +PackageSummary: Tauri is a rust project that enables developers to make secure +and small desktop applications using a web frontend. + +PackageComment: The package includes the following libraries; see +Relationship information. + +Created: 2019-05-20T09:00:00Z +PackageDownloadLocation: git://github.com/tauri-apps/tauri +PackageDownloadLocation: git+https://github.com/tauri-apps/tauri.git +PackageDownloadLocation: git+ssh://github.com/tauri-apps/tauri.git +Creator: Person: Daniel Thompson-Yvetot \ No newline at end of file diff --git a/plugins/fs/LICENSE_APACHE-2.0 b/plugins/fs/LICENSE_APACHE-2.0 new file mode 100644 index 0000000..4947287 --- /dev/null +++ b/plugins/fs/LICENSE_APACHE-2.0 @@ -0,0 +1,177 @@ + + 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 \ No newline at end of file diff --git a/plugins/fs/LICENSE_MIT b/plugins/fs/LICENSE_MIT new file mode 100644 index 0000000..4d75472 --- /dev/null +++ b/plugins/fs/LICENSE_MIT @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2017 - Present Tauri Apps 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. \ No newline at end of file diff --git a/plugins/fs/README.md b/plugins/fs/README.md new file mode 100644 index 0000000..d02c71d --- /dev/null +++ b/plugins/fs/README.md @@ -0,0 +1,91 @@ +![plugin-fs](https://github.com/tauri-apps/plugins-workspace/raw/v2/plugins/fs/banner.png) + +Access the file system. + +| Platform | Supported | +| -------- | --------- | +| Linux | ✓ | +| Windows | ✓ | +| macOS | ✓ | +| Android | ✓ | +| iOS | ✓ | + +## Install + +_This plugin requires a Rust version of at least **1.77.2**_ + +There are three general methods of installation that we can recommend. + +1. Use crates.io and npm (easiest, and requires you to trust that our publishing pipeline worked) +2. Pull sources directly from Github using git tags / revision hashes (most secure) +3. Git submodule install this repo in your tauri project and then use file protocol to ingest the source (most secure, but inconvenient to use) + +Install the Core plugin by adding the following to your `Cargo.toml` file: + +`src-tauri/Cargo.toml` + +```toml +[dependencies] +tauri-plugin-fs = "2.0.0" +# alternatively with Git: +tauri-plugin-fs = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v2" } +``` + +You can install the JavaScript Guest bindings using your preferred JavaScript package manager: + +```sh +pnpm add @tauri-apps/plugin-fs +# or +npm add @tauri-apps/plugin-fs +# or +yarn add @tauri-apps/plugin-fs +``` + +## Usage + +First you need to register the core plugin with Tauri: + +`src-tauri/src/lib.rs` + +```rust +fn main() { + tauri::Builder::default() + .plugin(tauri_plugin_fs::init()) + .run(tauri::generate_context!()) + .expect("error while running tauri application"); +} +``` + +Afterwards all the plugin's APIs are available through the JavaScript guest bindings: + +```javascript +import { stat } from '@tauri-apps/plugin-fs' + +await stat('/path/to/file') +``` + +## Contributing + +PRs accepted. Please make sure to read the Contributing Guide before making a pull request. + +## Partners + + + + + + + +
+ + CrabNebula + +
+ +For the complete list of sponsors please visit our [website](https://tauri.app#sponsors) and [Open Collective](https://opencollective.com/tauri). + +## License + +Code: (c) 2015 - Present - The Tauri Programme within The Commons Conservancy. + +MIT or MIT/Apache 2.0 where applicable. diff --git a/plugins/fs/SECURITY.md b/plugins/fs/SECURITY.md new file mode 100644 index 0000000..838ed67 --- /dev/null +++ b/plugins/fs/SECURITY.md @@ -0,0 +1,49 @@ +# Security Policy + +**Do not report security vulnerabilities through public GitHub issues.** + +**Please use the [Private Vulnerability Disclosure](https://docs.github.com/en/code-security/security-advisories/guidance-on-reporting-and-writing-information-about-vulnerabilities/privately-reporting-a-security-vulnerability#privately-reporting-a-security-vulnerability) feature of GitHub.** + +Include as much of the following information: + +- Type of issue (e.g. improper input parsing, privilege escalation, etc.) +- The location of the affected source code (tag/branch/commit or direct URL) +- Any special configuration required to reproduce the issue +- The distribution affected or used to help us with reproduction of the issue +- Step-by-step instructions to reproduce the issue +- Ideally a reproduction repository +- Impact of the issue, including how an attacker might exploit the issue + +We prefer to receive reports in English. + +## Contact + +Please disclose a vulnerability or security relevant issue here: [https://github.com/tauri-apps/plugins-workspace/security/advisories/new](https://github.com/tauri-apps/plugins-workspace/security/advisories/new). + +Alternatively, you can also contact us by email via [security@tauri.app](mailto:security@tauri.app). + +## Threat Model + +This plugin possibly allows access to the full filesystem available to the application process. +Depending on the operating system the access is already confined (android/ios) to only certain locations. +In other operating systems like Linux/MacOS/Windows it depends on the installation and packaging method but in most cases full +access is granted. + +To prevent exposure of sensitive locations and data this plugin can be scoped to only allow certain base directories +or only access to specific files or subdirectories. +This scoping effectively affects only calls made from the webviews/frontend code and calls made from rust can always circumvent +the restrictions imposed by the scope. + +The scope is defined at compile time in the used permissions but the user or application developer can grant or revoke access to specific files or folders at runtime by modifying the scope state through the runtime authority, if configured during plugin initialization. + +### Security Assumptions + +- The filesystem access is limited by user permissions +- The operating system filesystem access confinment works as documented +- The scoping mechanism of the Tauri `fs` commands work as intended and has no bypasses +- The user or application developer can grant or revoke access to specific files at runtime by modifying the scope + +#### Out Of Scope + +- Exploits in underlying filesystems +- Exploits in the underlying rust `std::fs` library diff --git a/plugins/fs/android/.gitignore b/plugins/fs/android/.gitignore new file mode 100644 index 0000000..c0f21ec --- /dev/null +++ b/plugins/fs/android/.gitignore @@ -0,0 +1,2 @@ +/build +/.tauri diff --git a/plugins/fs/android/build.gradle.kts b/plugins/fs/android/build.gradle.kts new file mode 100644 index 0000000..b1b6af1 --- /dev/null +++ b/plugins/fs/android/build.gradle.kts @@ -0,0 +1,36 @@ +plugins { + id("com.android.library") + id("org.jetbrains.kotlin.android") +} + +android { + namespace = "com.plugin.fs" + compileSdk = 36 + + defaultConfig { + minSdk = 21 + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + consumerProguardFiles("consumer-rules.pro") + } + + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 + } + kotlinOptions { + jvmTarget = "1.8" + } +} + +dependencies { + + implementation("androidx.core:core-ktx:1.9.0") + implementation("androidx.appcompat:appcompat:1.6.0") + implementation("com.google.android.material:material:1.7.0") + testImplementation("junit:junit:4.13.2") + androidTestImplementation("androidx.test.ext:junit:1.1.5") + androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1") + implementation(project(":tauri-android")) +} diff --git a/plugins/fs/android/consumer-rules.pro b/plugins/fs/android/consumer-rules.pro new file mode 100644 index 0000000..481bb43 --- /dev/null +++ b/plugins/fs/android/consumer-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/plugins/fs/android/settings.gradle b/plugins/fs/android/settings.gradle new file mode 100644 index 0000000..d7782a4 --- /dev/null +++ b/plugins/fs/android/settings.gradle @@ -0,0 +1,31 @@ +pluginManagement { + repositories { + mavenCentral() + gradlePluginPortal() + google() + } + resolutionStrategy { + eachPlugin { + switch (requested.id.id) { + case "com.android.library": + useVersion("8.0.2") + break + case "org.jetbrains.kotlin.android": + useVersion("1.8.20") + break + } + } + } +} + +dependencyResolutionManagement { + repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) + repositories { + mavenCentral() + google() + + } +} + +include ':tauri-android' +project(':tauri-android').projectDir = new File('./.tauri/tauri-api') diff --git a/plugins/fs/android/src/androidTest/java/ExampleInstrumentedTest.kt b/plugins/fs/android/src/androidTest/java/ExampleInstrumentedTest.kt new file mode 100644 index 0000000..c3b473f --- /dev/null +++ b/plugins/fs/android/src/androidTest/java/ExampleInstrumentedTest.kt @@ -0,0 +1,28 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +package com.plugin.fs + +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.ext.junit.runners.AndroidJUnit4 + +import org.junit.Test +import org.junit.runner.RunWith + +import org.junit.Assert.* + +/** + * Instrumented test, which will execute on an Android device. + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +@RunWith(AndroidJUnit4::class) +class ExampleInstrumentedTest { + @Test + fun useAppContext() { + // Context of the app under test. + val appContext = InstrumentationRegistry.getInstrumentation().targetContext + assertEquals("com.plugin.fs", appContext.packageName) + } +} diff --git a/plugins/fs/android/src/main/AndroidManifest.xml b/plugins/fs/android/src/main/AndroidManifest.xml new file mode 100644 index 0000000..9a40236 --- /dev/null +++ b/plugins/fs/android/src/main/AndroidManifest.xml @@ -0,0 +1,3 @@ + + + diff --git a/plugins/fs/android/src/main/java/FsPlugin.kt b/plugins/fs/android/src/main/java/FsPlugin.kt new file mode 100644 index 0000000..877fbf4 --- /dev/null +++ b/plugins/fs/android/src/main/java/FsPlugin.kt @@ -0,0 +1,93 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +package com.plugin.fs + +import android.annotation.SuppressLint +import android.app.Activity +import android.content.res.AssetManager.ACCESS_BUFFER +import android.net.Uri +import android.os.ParcelFileDescriptor +import app.tauri.annotation.Command +import app.tauri.annotation.InvokeArg +import app.tauri.annotation.TauriPlugin +import app.tauri.plugin.Invoke +import app.tauri.plugin.JSObject +import app.tauri.plugin.Plugin +import java.io.File +import java.io.FileOutputStream +import java.io.IOException +import java.io.InputStream +import java.io.OutputStream + +@InvokeArg +class WriteTextFileArgs { + val uri: String = "" + val content: String = "" +} + +@InvokeArg +class GetFileDescriptorArgs { + lateinit var uri: String + lateinit var mode: String +} + +@TauriPlugin +class FsPlugin(private val activity: Activity): Plugin(activity) { + @SuppressLint("Recycle") + @Command + fun getFileDescriptor(invoke: Invoke) { + val args = invoke.parseArgs(GetFileDescriptorArgs::class.java) + + val res = JSObject() + + if (args.uri.startsWith(app.tauri.TAURI_ASSETS_DIRECTORY_URI)) { + val path = args.uri.substring(app.tauri.TAURI_ASSETS_DIRECTORY_URI.length) + try { + val fd = activity.assets.openFd(path).parcelFileDescriptor?.detachFd() + res.put("fd", fd) + } catch (e: IOException) { + // if the asset is compressed, we cannot open a file descriptor directly + // so we copy it to the cache and get a fd from there + // this is a lot faster than serializing the file and sending it as invoke response + // because on the Rust side we can leverage the custom protocol IPC and read the file directly + val cacheFile = File(activity.cacheDir, "_assets/$path") + cacheFile.parentFile?.mkdirs() + copyAsset(path, cacheFile) + + val fd = ParcelFileDescriptor.open(cacheFile, ParcelFileDescriptor.parseMode(args.mode)).detachFd() + res.put("fd", fd) + } + } else { + val fd = activity.contentResolver.openAssetFileDescriptor( + Uri.parse(args.uri), + args.mode + )?.parcelFileDescriptor?.detachFd() + res.put("fd", fd) + } + + invoke.resolve(res) + } + + @Throws(IOException::class) + private fun copy(input: InputStream, output: OutputStream) { + val buf = ByteArray(1024) + var len: Int + while ((input.read(buf).also { len = it }) > 0) { + output.write(buf, 0, len) + } + } + + @Throws(IOException::class) + private fun copyAsset(assetPath: String, cacheFile: File) { + val input = activity.assets.open(assetPath, ACCESS_BUFFER) + input.use { i -> + val output = FileOutputStream(cacheFile, false) + output.use { o -> + copy(i, o) + } + } + } +} + diff --git a/plugins/fs/android/src/test/java/ExampleUnitTest.kt b/plugins/fs/android/src/test/java/ExampleUnitTest.kt new file mode 100644 index 0000000..340839a --- /dev/null +++ b/plugins/fs/android/src/test/java/ExampleUnitTest.kt @@ -0,0 +1,21 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +package com.plugin.fs + +import org.junit.Test + +import org.junit.Assert.* + +/** + * Example local unit test, which will execute on the development machine (host). + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +class ExampleUnitTest { + @Test + fun addition_isCorrect() { + assertEquals(4, 2 + 2) + } +} diff --git a/plugins/fs/api-iife.js b/plugins/fs/api-iife.js new file mode 100644 index 0000000..ba06038 --- /dev/null +++ b/plugins/fs/api-iife.js @@ -0,0 +1 @@ +if("__TAURI__"in window){var __TAURI_PLUGIN_FS__=function(e,n,t){"use strict";var i;function o(e){return{isFile:e.isFile,isDirectory:e.isDirectory,isSymlink:e.isSymlink,size:e.size,mtime:null!==e.mtime?new Date(e.mtime):null,atime:null!==e.atime?new Date(e.atime):null,birthtime:null!==e.birthtime?new Date(e.birthtime):null,readonly:e.readonly,fileAttributes:e.fileAttributes,dev:e.dev,ino:e.ino,mode:e.mode,nlink:e.nlink,uid:e.uid,gid:e.gid,rdev:e.rdev,blksize:e.blksize,blocks:e.blocks}}e.SeekMode=void 0,(i=e.SeekMode||(e.SeekMode={}))[i.Start=0]="Start",i[i.Current=1]="Current",i[i.End=2]="End";class r extends t.Resource{async read(e){if(0===e.byteLength)return 0;const n=await t.invoke("plugin:fs|read",{rid:this.rid,len:e.byteLength}),i=function(e){const n=new Uint8ClampedArray(e),t=n.byteLength;let i=0;for(let e=0;ee instanceof URL?e.toString():e),options:i,onEvent:r}),c=new s(a);return()=>{c.close()}}return Object.defineProperty(e,"BaseDirectory",{enumerable:!0,get:function(){return n.BaseDirectory}}),e.FileHandle=r,e.copyFile=async function(e,n,i){if(e instanceof URL&&"file:"!==e.protocol||n instanceof URL&&"file:"!==n.protocol)throw new TypeError("Must be a file URL.");await t.invoke("plugin:fs|copy_file",{fromPath:e instanceof URL?e.toString():e,toPath:n instanceof URL?n.toString():n,options:i})},e.create=async function(e,n){if(e instanceof URL&&"file:"!==e.protocol)throw new TypeError("Must be a file URL.");const i=await t.invoke("plugin:fs|create",{path:e instanceof URL?e.toString():e,options:n});return new r(i)},e.exists=async function(e,n){if(e instanceof URL&&"file:"!==e.protocol)throw new TypeError("Must be a file URL.");return await t.invoke("plugin:fs|exists",{path:e instanceof URL?e.toString():e,options:n})},e.lstat=async function(e,n){return o(await t.invoke("plugin:fs|lstat",{path:e instanceof URL?e.toString():e,options:n}))},e.mkdir=async function(e,n){if(e instanceof URL&&"file:"!==e.protocol)throw new TypeError("Must be a file URL.");await t.invoke("plugin:fs|mkdir",{path:e instanceof URL?e.toString():e,options:n})},e.open=a,e.readDir=async function(e,n){if(e instanceof URL&&"file:"!==e.protocol)throw new TypeError("Must be a file URL.");return await t.invoke("plugin:fs|read_dir",{path:e instanceof URL?e.toString():e,options:n})},e.readFile=async function(e,n){if(e instanceof URL&&"file:"!==e.protocol)throw new TypeError("Must be a file URL.");const i=await t.invoke("plugin:fs|read_file",{path:e instanceof URL?e.toString():e,options:n});return i instanceof ArrayBuffer?new Uint8Array(i):Uint8Array.from(i)},e.readTextFile=async function(e,n){if(e instanceof URL&&"file:"!==e.protocol)throw new TypeError("Must be a file URL.");const i=await t.invoke("plugin:fs|read_text_file",{path:e instanceof URL?e.toString():e,options:n}),o=i instanceof ArrayBuffer?i:Uint8Array.from(i);return new TextDecoder(n?.encoding??"utf-8").decode(o)},e.readTextFileLines=async function(e,n){if(e instanceof URL&&"file:"!==e.protocol)throw new TypeError("Must be a file URL.");const i=e instanceof URL?e.toString():e;return await Promise.resolve({path:i,rid:null,async next(){const e=new TextDecoder(n?.encoding??"utf-8");if(null===this.rid){const o=e.encoding;this.rid=await t.invoke("plugin:fs|read_text_file_lines",{path:i,options:null!=n?{...n,encoding:o}:void 0})}const o=await t.invoke("plugin:fs|read_text_file_lines_next",{rid:this.rid}),r=o instanceof ArrayBuffer?new Uint8Array(o):Uint8Array.from(o),a=1===r[r.byteLength-1];if(a)return this.rid=null,{value:null,done:a};return{value:e.decode(r.slice(0,r.byteLength-1)),done:a}},[Symbol.asyncIterator](){return this}})},e.remove=async function(e,n){if(e instanceof URL&&"file:"!==e.protocol)throw new TypeError("Must be a file URL.");await t.invoke("plugin:fs|remove",{path:e instanceof URL?e.toString():e,options:n})},e.rename=async function(e,n,i){if(e instanceof URL&&"file:"!==e.protocol||n instanceof URL&&"file:"!==n.protocol)throw new TypeError("Must be a file URL.");await t.invoke("plugin:fs|rename",{oldPath:e instanceof URL?e.toString():e,newPath:n instanceof URL?n.toString():n,options:i})},e.size=async function(e){if(e instanceof URL&&"file:"!==e.protocol)throw new TypeError("Must be a file URL.");return await t.invoke("plugin:fs|size",{path:e instanceof URL?e.toString():e})},e.startAccessingSecurityScopedResource=async function(e){if(e instanceof URL&&"file:"!==e.protocol)throw new TypeError("Must be a file URL.");await t.invoke("plugin:fs|start_accessing_security_scoped_resource",{path:e instanceof URL?e.toString():e})},e.stat=async function(e,n){return o(await t.invoke("plugin:fs|stat",{path:e instanceof URL?e.toString():e,options:n}))},e.stopAccessingSecurityScopedResource=async function(e){if(e instanceof URL&&"file:"!==e.protocol)throw new TypeError("Must be a file URL.");await t.invoke("plugin:fs|stop_accessing_security_scoped_resource",{path:e instanceof URL?e.toString():e})},e.truncate=async function(e,n,i){if(e instanceof URL&&"file:"!==e.protocol)throw new TypeError("Must be a file URL.");await t.invoke("plugin:fs|truncate",{path:e instanceof URL?e.toString():e,len:n,options:i})},e.watch=async function(e,n,t){return await c(e,n,{delayMs:2e3,...t})},e.watchImmediate=async function(e,n,t){return await c(e,n,{...t,delayMs:void 0})},e.writeFile=async function(e,n,i){if(e instanceof URL&&"file:"!==e.protocol)throw new TypeError("Must be a file URL.");if(n instanceof ReadableStream){const t=await a(e,{read:!1,create:!0,write:!0,...i}),o=n.getReader();try{for(;;){const{done:e,value:n}=await o.read();if(e)break;await t.write(n)}}finally{o.releaseLock(),await t.close()}}else await t.invoke("plugin:fs|write_file",n,{headers:{path:encodeURIComponent(e instanceof URL?e.toString():e),options:JSON.stringify(i)}})},e.writeTextFile=async function(e,n,i){if(e instanceof URL&&"file:"!==e.protocol)throw new TypeError("Must be a file URL.");const o=new TextEncoder;await t.invoke("plugin:fs|write_text_file",o.encode(n),{headers:{path:encodeURIComponent(e instanceof URL?e.toString():e),options:JSON.stringify(i)}})},e}({},window.__TAURI__.path,window.__TAURI__.core);Object.defineProperty(window.__TAURI__,"fs",{value:__TAURI_PLUGIN_FS__})} diff --git a/plugins/fs/banner.png b/plugins/fs/banner.png new file mode 100644 index 0000000000000000000000000000000000000000..767a1fe20ec1a9e881638f4939f56a4f075501dd GIT binary patch literal 50877 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 literal 0 HcmV?d00001 diff --git a/plugins/fs/build.rs b/plugins/fs/build.rs new file mode 100644 index 0000000..34b9475 --- /dev/null +++ b/plugins/fs/build.rs @@ -0,0 +1,269 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use std::{ + fs::create_dir_all, + path::{Path, PathBuf}, +}; + +use tauri_utils::acl::manifest::PermissionFile; + +#[path = "src/scope.rs"] +#[allow(dead_code)] +mod scope; + +/// FS scope entry. +#[derive(schemars::JsonSchema)] +#[serde(untagged)] +#[allow(unused)] +enum FsScopeEntry { + /// A path that can be accessed by the webview when using the fs APIs. + /// FS scope path pattern. + /// + /// The pattern can start with a variable that resolves to a system base directory. + /// The variables are: `$AUDIO`, `$CACHE`, `$CONFIG`, `$DATA`, `$LOCALDATA`, `$DESKTOP`, + /// `$DOCUMENT`, `$DOWNLOAD`, `$EXE`, `$FONT`, `$HOME`, `$PICTURE`, `$PUBLIC`, `$RUNTIME`, + /// `$TEMPLATE`, `$VIDEO`, `$RESOURCE`, `$APP`, `$LOG`, `$TEMP`, `$APPCONFIG`, `$APPDATA`, + /// `$APPLOCALDATA`, `$APPCACHE`, `$APPLOG`. + Value(PathBuf), + Object { + /// A path that can be accessed by the webview when using the fs APIs. + /// + /// The pattern can start with a variable that resolves to a system base directory. + /// The variables are: `$AUDIO`, `$CACHE`, `$CONFIG`, `$DATA`, `$LOCALDATA`, `$DESKTOP`, + /// `$DOCUMENT`, `$DOWNLOAD`, `$EXE`, `$FONT`, `$HOME`, `$PICTURE`, `$PUBLIC`, `$RUNTIME`, + /// `$TEMPLATE`, `$VIDEO`, `$RESOURCE`, `$APP`, `$LOG`, `$TEMP`, `$APPCONFIG`, `$APPDATA`, + /// `$APPLOCALDATA`, `$APPCACHE`, `$APPLOG`. + path: PathBuf, + }, +} + +// Ensure `FsScopeEntry` and `scope::EntryRaw` is kept in sync +fn _f() { + match scope::EntryRaw::Value(PathBuf::new()) { + scope::EntryRaw::Value(path) => FsScopeEntry::Value(path), + scope::EntryRaw::Object { path } => FsScopeEntry::Object { path }, + }; + match FsScopeEntry::Value(PathBuf::new()) { + FsScopeEntry::Value(path) => scope::EntryRaw::Value(path), + FsScopeEntry::Object { path } => scope::EntryRaw::Object { path }, + }; +} + +const BASE_DIR_VARS: &[&str] = &[ + "AUDIO", + "CACHE", + "CONFIG", + "DATA", + "LOCALDATA", + "DESKTOP", + "DOCUMENT", + "DOWNLOAD", + "EXE", + "FONT", + "HOME", + "PICTURE", + "PUBLIC", + "RUNTIME", + "TEMPLATE", + "VIDEO", + "RESOURCE", + "LOG", + "TEMP", + "APPCONFIG", + "APPDATA", + "APPLOCALDATA", + "APPCACHE", + "APPLOG", +]; +const COMMANDS: &[(&str, &[&str])] = &[ + ("mkdir", &[]), + ("create", &[]), + ("copy_file", &[]), + ("remove", &[]), + ("rename", &[]), + ("truncate", &[]), + ("ftruncate", &[]), + ("write", &[]), + ("write_file", &["open", "write"]), + ("write_text_file", &[]), + ("read_dir", &[]), + ("read_file", &[]), + ("read", &[]), + ("open", &[]), + ("read_text_file", &[]), + ("read_text_file_lines", &["read_text_file_lines_next"]), + ("read_text_file_lines_next", &[]), + ("seek", &[]), + ("stat", &[]), + ("lstat", &[]), + ("fstat", &[]), + ("exists", &[]), + ("watch", &[]), + // TODO: Remove this in v3 + ("unwatch", &[]), + ("size", &[]), + ("start_accessing_security_scoped_resource", &[]), + ("stop_accessing_security_scoped_resource", &[]), +]; + +fn main() { + let autogenerated = Path::new("permissions/autogenerated/"); + let base_dirs = &autogenerated.join("base-directories"); + + if !base_dirs.exists() { + create_dir_all(base_dirs).expect("unable to create autogenerated base directories dir"); + } + + for base_dir in BASE_DIR_VARS { + let upper = base_dir; + let lower = base_dir.to_lowercase(); + let toml = format!( + r###"# Automatically generated - DO NOT EDIT! + +"$schema" = "../../schemas/schema.json" + +# Scopes Section +# This section contains scopes, which define file level access + +[[permission]] +identifier = "scope-{lower}-recursive" +description = "This scope permits recursive access to the complete `${upper}` folder, including sub directories and files." + +[[permission.scope.allow]] +path = "${upper}" +[[permission.scope.allow]] +path = "${upper}/**" + +[[permission]] +identifier = "scope-{lower}" +description = "This scope permits access to all files and list content of top level directories in the `${upper}` folder." + +[[permission.scope.allow]] +path = "${upper}" +[[permission.scope.allow]] +path = "${upper}/*" + +[[permission]] +identifier = "scope-{lower}-index" +description = "This scope permits to list all files and folders in the `${upper}`folder." + +[[permission.scope.allow]] +path = "${upper}" + +# Sets Section +# This section combines the scope elements with enablement of commands + +[[set]] +identifier = "allow-{lower}-read-recursive" +description = "This allows full recursive read access to the complete `${upper}` folder, files and subdirectories." +permissions = [ + "read-all", + "scope-{lower}-recursive" +] + +[[set]] +identifier = "allow-{lower}-write-recursive" +description = "This allows full recursive write access to the complete `${upper}` folder, files and subdirectories." +permissions = [ + "write-all", + "scope-{lower}-recursive" +] + +[[set]] +identifier = "allow-{lower}-read" +description = "This allows non-recursive read access to the `${upper}` folder." +permissions = [ + "read-all", + "scope-{lower}" +] + +[[set]] +identifier = "allow-{lower}-write" +description = "This allows non-recursive write access to the `${upper}` folder." +permissions = [ + "write-all", + "scope-{lower}" +] + +[[set]] +identifier = "allow-{lower}-meta-recursive" +description = "This allows full recursive read access to metadata of the `${upper}` folder, including file listing and statistics." +permissions = [ + "read-meta", + "scope-{lower}-recursive" +] + +[[set]] +identifier = "allow-{lower}-meta" +description = "This allows non-recursive read access to metadata of the `${upper}` folder, including file listing and statistics." +permissions = [ + "read-meta", + "scope-{lower}-index" +]"### + ); + + let permission_path = base_dirs.join(format!("{lower}.toml")); + if toml != std::fs::read_to_string(&permission_path).unwrap_or_default() { + std::fs::write(permission_path, toml) + .unwrap_or_else(|e| panic!("unable to autogenerate ${lower}: {e}")); + } + } + + tauri_plugin::Builder::new( + &COMMANDS + .iter() + // FIXME: https://docs.rs/crate/tauri-plugin-fs/2.1.0/builds/1571296 + .filter(|c| c.1.is_empty()) + .map(|c| c.0) + .collect::>(), + ) + .global_api_script_path("./api-iife.js") + .global_scope_schema(schemars::schema_for!(FsScopeEntry)) + .android_path("android") + .build(); + + // workaround to include nested permissions as `tauri_plugin` doesn't support it + let permissions_dir = autogenerated.join("commands"); + for (command, nested_commands) in COMMANDS { + if nested_commands.is_empty() { + continue; + } + + let permission_path = permissions_dir.join(format!("{command}.toml")); + + let content = std::fs::read_to_string(&permission_path) + .unwrap_or_else(|_| panic!("failed to read {command}.toml")); + + let mut permission_file = toml::from_str::(&content) + .unwrap_or_else(|_| panic!("failed to deserialize {command}.toml")); + + for p in permission_file + .permission + .iter_mut() + .filter(|p| p.identifier.starts_with("allow")) + { + for c in nested_commands.iter().map(|s| s.to_string()) { + if !p.commands.allow.contains(&c) { + p.commands.allow.push(c); + } + } + } + + let out = toml::to_string_pretty(&permission_file) + .unwrap_or_else(|_| panic!("failed to serialize {command}.toml")); + let out = format!( + r#"# Automatically generated - DO NOT EDIT! + +"$schema" = "../../schemas/schema.json" + +{out}"# + ); + + if content != out { + std::fs::write(permission_path, out) + .unwrap_or_else(|_| panic!("failed to write {command}.toml")); + } + } +} diff --git a/plugins/fs/guest-js/index.ts b/plugins/fs/guest-js/index.ts new file mode 100644 index 0000000..2bea269 --- /dev/null +++ b/plugins/fs/guest-js/index.ts @@ -0,0 +1,1649 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +/** + * Access the file system. + * + * ## iOS security-scoped resources + * + * On iOS, the `fs` plugin automatically manages access to security-scoped resources when a file URL is accessed. + * This is required for files outside the app's sandbox (e.g., from file picker). + * + * @example + * ```typescript + * import { open } from '@tauri-apps/plugin-fs'; + * + * const file = await open('file:///path/to/file.txt'); + * await file.close(); + * ``` + * + * ## Security + * + * This module prevents path traversal, not allowing parent directory accessors to be used + * (i.e. "/usr/path/to/../file" or "../path/to/file" paths are not allowed). + * Paths accessed with this API must be either relative to one of the {@link BaseDirectory | base directories} + * or created with the {@link https://v2.tauri.app/reference/javascript/api/namespacepath/ | path API}. + * + * The API has a scope configuration that forces you to restrict the paths that can be accessed using glob patterns. + * + * The scope configuration is an array of glob patterns describing file/directory paths that are allowed. + * For instance, this scope configuration allows **all** enabled `fs` APIs to (only) access files in the + * *databases* directory of the {@link https://v2.tauri.app/reference/javascript/api/namespacepath/#appdatadir | `$APPDATA` directory}: + * ```json + * { + * "permissions": [ + * { + * "identifier": "fs:scope", + * "allow": [{ "path": "$APPDATA/databases/*" }] + * } + * ] + * } + * ``` + * + * Scopes can also be applied to specific `fs` APIs by using the API's identifier instead of `fs:scope`: + * ```json + * { + * "permissions": [ + * { + * "identifier": "fs:allow-exists", + * "allow": [{ "path": "$APPDATA/databases/*" }] + * } + * ] + * } + * ``` + * + * Notice the use of the `$APPDATA` variable. The value is injected at runtime, resolving to the {@link https://v2.tauri.app/reference/javascript/api/namespacepath/#appdatadir | app data directory}. + * + * The available variables are: + * {@linkcode https://v2.tauri.app/reference/javascript/api/namespacepath/#appconfigdir | $APPCONFIG}, + * {@linkcode https://v2.tauri.app/reference/javascript/api/namespacepath/#appdatadir | $APPDATA}, + * {@linkcode https://v2.tauri.app/reference/javascript/api/namespacepath/#applocaldatadir | $APPLOCALDATA}, + * {@linkcode https://v2.tauri.app/reference/javascript/api/namespacepath/#appcachedir | $APPCACHE}, + * {@linkcode https://v2.tauri.app/reference/javascript/api/namespacepath/#applogdir | $APPLOG}, + * {@linkcode https://v2.tauri.app/reference/javascript/api/namespacepath/#audiodir | $AUDIO}, + * {@linkcode https://v2.tauri.app/reference/javascript/api/namespacepath/#cachedir | $CACHE}, + * {@linkcode https://v2.tauri.app/reference/javascript/api/namespacepath/#configdir | $CONFIG}, + * {@linkcode https://v2.tauri.app/reference/javascript/api/namespacepath/#datadir | $DATA}, + * {@linkcode https://v2.tauri.app/reference/javascript/api/namespacepath/#localdatadir | $LOCALDATA}, + * {@linkcode https://v2.tauri.app/reference/javascript/api/namespacepath/#desktopdir | $DESKTOP}, + * {@linkcode https://v2.tauri.app/reference/javascript/api/namespacepath/#documentdir | $DOCUMENT}, + * {@linkcode https://v2.tauri.app/reference/javascript/api/namespacepath/#downloaddir | $DOWNLOAD}, + * {@linkcode https://v2.tauri.app/reference/javascript/api/namespacepath/#executabledir | $EXE}, + * {@linkcode https://v2.tauri.app/reference/javascript/api/namespacepath/#fontdir | $FONT}, + * {@linkcode https://v2.tauri.app/reference/javascript/api/namespacepath/#homedir | $HOME}, + * {@linkcode https://v2.tauri.app/reference/javascript/api/namespacepath/#picturedir | $PICTURE}, + * {@linkcode https://v2.tauri.app/reference/javascript/api/namespacepath/#publicdir | $PUBLIC}, + * {@linkcode https://v2.tauri.app/reference/javascript/api/namespacepath/#runtimedir | $RUNTIME}, + * {@linkcode https://v2.tauri.app/reference/javascript/api/namespacepath/#templatedir | $TEMPLATE}, + * {@linkcode https://v2.tauri.app/reference/javascript/api/namespacepath/#videodir | $VIDEO}, + * {@linkcode https://v2.tauri.app/reference/javascript/api/namespacepath/#resourcedir | $RESOURCE}, + * {@linkcode https://v2.tauri.app/reference/javascript/api/namespacepath/#tempdir | $TEMP}. + * + * Trying to execute any API with a URL not configured on the scope results in a promise rejection due to denied access. + * + * @module + */ + +import { BaseDirectory } from '@tauri-apps/api/path' +import { Channel, invoke, Resource } from '@tauri-apps/api/core' + +/** + * Defines how the offset given to {@linkcode FileHandle.seek} is interpreted. + */ +enum SeekMode { + /** The offset is relative to the start of the file. */ + Start = 0, + /** The offset is relative to the current cursor position. */ + Current = 1, + /** The offset is relative to the end of the file. */ + End = 2 +} + +/** + * A FileInfo describes a file and is returned by `stat`, `lstat` or `fstat`. + * + * @since 2.0.0 + */ +interface FileInfo { + /** + * True if this is info for a regular file. Mutually exclusive to + * `FileInfo.isDirectory` and `FileInfo.isSymlink`. + */ + isFile: boolean + /** + * True if this is info for a regular directory. Mutually exclusive to + * `FileInfo.isFile` and `FileInfo.isSymlink`. + */ + isDirectory: boolean + /** + * True if this is info for a symlink. Mutually exclusive to + * `FileInfo.isFile` and `FileInfo.isDirectory`. + */ + isSymlink: boolean + /** + * The size of the file, in bytes. + */ + size: number + /** + * The last modification time of the file. This corresponds to the `mtime` + * field from `stat` on Linux/Mac OS and `ftLastWriteTime` on Windows. This + * may not be available on all platforms. + */ + mtime: Date | null + /** + * The last access time of the file. This corresponds to the `atime` + * field from `stat` on Unix and `ftLastAccessTime` on Windows. This may not + * be available on all platforms. + */ + atime: Date | null + /** + * The creation time of the file. This corresponds to the `birthtime` + * field from `stat` on Mac/BSD and `ftCreationTime` on Windows. This may + * not be available on all platforms. + */ + birthtime: Date | null + /** Whether this is a readonly (unwritable) file. */ + readonly: boolean + /** + * This field contains the file system attribute information for a file + * or directory. For possible values and their descriptions, see + * {@link https://docs.microsoft.com/en-us/windows/win32/fileio/file-attribute-constants | File Attribute Constants} in the Windows Dev Center + * + * #### Platform-specific + * + * - **macOS / Linux / Android / iOS:** Unsupported. + */ + fileAttributes: number | null + /** + * ID of the device containing the file. + * + * #### Platform-specific + * + * - **Windows:** Unsupported. + */ + dev: number | null + /** + * Inode number. + * + * #### Platform-specific + * + * - **Windows:** Unsupported. + */ + ino: number | null + /** + * The underlying raw `st_mode` bits that contain the standard Unix + * permissions for this file/directory. + * + * #### Platform-specific + * + * - **Windows:** Unsupported. + */ + mode: number | null + /** + * Number of hard links pointing to this file. + * + * #### Platform-specific + * + * - **Windows:** Unsupported. + */ + nlink: number | null + /** + * User ID of the owner of this file. + * + * #### Platform-specific + * + * - **Windows:** Unsupported. + */ + uid: number | null + /** + * Group ID of the owner of this file. + * + * #### Platform-specific + * + * - **Windows:** Unsupported. + */ + gid: number | null + /** + * Device ID of this file. + * + * #### Platform-specific + * + * - **Windows:** Unsupported. + */ + rdev: number | null + /** + * Blocksize for filesystem I/O. + * + * #### Platform-specific + * + * - **Windows:** Unsupported. + */ + blksize: number | null + /** + * Number of blocks allocated to the file, in 512-byte units. + * + * #### Platform-specific + * + * - **Windows:** Unsupported. + */ + blocks: number | null +} + +interface UnparsedFileInfo { + isFile: boolean + isDirectory: boolean + isSymlink: boolean + size: number + mtime: number | null + atime: number | null + birthtime: number | null + readonly: boolean + fileAttributes: number + dev: number | null + ino: number | null + mode: number | null + nlink: number | null + uid: number | null + gid: number | null + rdev: number | null + blksize: number | null + blocks: number | null +} +function parseFileInfo(r: UnparsedFileInfo): FileInfo { + return { + isFile: r.isFile, + isDirectory: r.isDirectory, + isSymlink: r.isSymlink, + size: r.size, + mtime: r.mtime !== null ? new Date(r.mtime) : null, + atime: r.atime !== null ? new Date(r.atime) : null, + birthtime: r.birthtime !== null ? new Date(r.birthtime) : null, + readonly: r.readonly, + fileAttributes: r.fileAttributes, + dev: r.dev, + ino: r.ino, + mode: r.mode, + nlink: r.nlink, + uid: r.uid, + gid: r.gid, + rdev: r.rdev, + blksize: r.blksize, + blocks: r.blocks + } +} + +// https://mstn.github.io/2018/06/08/fixed-size-arrays-in-typescript/ +type FixedSizeArray = ReadonlyArray & { + length: N +} + +// https://gist.github.com/zapthedingbat/38ebfbedd98396624e5b5f2ff462611d +/** Converts a big-endian eight byte array to number */ +function fromBytes(buffer: FixedSizeArray): number { + const bytes = new Uint8ClampedArray(buffer) + const size = bytes.byteLength + let x = 0 + for (let i = 0; i < size; i++) { + // eslint-disable-next-line security/detect-object-injection + const byte = bytes[i] + x *= 0x100 + x += byte + } + return x +} + +/** + * The Tauri abstraction for reading and writing files. + * + * @since 2.0.0 + */ +class FileHandle extends Resource { + /** + * Reads up to `p.byteLength` bytes into `p`. It resolves to the number of + * bytes read (`0` < `n` <= `p.byteLength`) and rejects if any error + * encountered. Even if `read()` resolves to `n` < `p.byteLength`, it may + * use all of `p` as scratch space during the call. If some data is + * available but not `p.byteLength` bytes, `read()` conventionally resolves + * to what is available instead of waiting for more. + * + * When `read()` encounters end-of-file condition, it resolves to EOF + * (`null`). + * + * When `read()` encounters an error, it rejects with an error. + * + * Callers should always process the `n` > `0` bytes returned before + * considering the EOF (`null`). Doing so correctly handles I/O errors that + * happen after reading some bytes and also both of the allowed EOF + * behaviors. + * + * @example + * ```typescript + * import { open, BaseDirectory } from "@tauri-apps/plugin-fs" + * // if "$APPCONFIG/foo/bar.txt" contains the text "hello world": + * const file = await open("foo/bar.txt", { baseDir: BaseDirectory.AppConfig }); + * const buf = new Uint8Array(100); + * const numberOfBytesRead = await file.read(buf); // 11 bytes + * const text = new TextDecoder().decode(buf); // "hello world" + * await file.close(); + * ``` + * + * @param buffer The buffer the file contents are read into. + * @returns A promise resolving to the number of bytes read, or `null` when the end of the file was reached. + * @since 2.0.0 + */ + async read(buffer: Uint8Array): Promise { + if (buffer.byteLength === 0) { + return 0 + } + + const data = await invoke('plugin:fs|read', { + rid: this.rid, + len: buffer.byteLength + }) + + // Rust side will never return an empty array for this command and + // ensure there is at least 8 elements there. + // + // This is an optimization to include the number of read bytes (as bigendian bytes) + // at the end of returned array to avoid serialization overhead of separate values. + const nread = fromBytes(data.slice(-8) as FixedSizeArray) + + const bytes = data instanceof ArrayBuffer ? new Uint8Array(data) : data + buffer.set(bytes.slice(0, bytes.length - 8)) + + return nread === 0 ? null : nread + } + + /** + * Seek sets the offset for the next `read()` or `write()` to offset, + * interpreted according to `whence`: `Start` means relative to the + * start of the file, `Current` means relative to the current offset, + * and `End` means relative to the end. Seek resolves to the new offset + * relative to the start of the file. + * + * Seeking to an offset before the start of the file is an error. Seeking to + * any positive offset is legal, but the behavior of subsequent I/O + * operations on the underlying object is implementation-dependent. + * It returns the number of cursor position. + * + * @example + * ```typescript + * import { open, SeekMode, BaseDirectory } from '@tauri-apps/plugin-fs'; + * + * // Given hello.txt pointing to file with "Hello world", which is 11 bytes long: + * const file = await open('hello.txt', { read: true, write: true, truncate: true, create: true, baseDir: BaseDirectory.AppLocalData }); + * await file.write(new TextEncoder().encode("Hello world")); + * + * // Seek 6 bytes from the start of the file + * console.log(await file.seek(6, SeekMode.Start)); // "6" + * // Seek 2 more bytes from the current position + * console.log(await file.seek(2, SeekMode.Current)); // "8" + * // Seek backwards 2 bytes from the end of the file + * console.log(await file.seek(-2, SeekMode.End)); // "9" (e.g. 11-2) + * + * await file.close(); + * ``` + * + * @param offset The number of bytes the cursor is moved by. + * @param whence Defines the position the `offset` is relative to. + * @returns A promise resolving to the new cursor position, relative to the start of the file. + * @since 2.0.0 + */ + async seek(offset: number, whence: SeekMode): Promise { + return await invoke('plugin:fs|seek', { + rid: this.rid, + offset, + whence + }) + } + + /** + * Returns a {@linkcode FileInfo } for this file. + * + * @example + * ```typescript + * import { open, BaseDirectory } from '@tauri-apps/plugin-fs'; + * const file = await open("file.txt", { read: true, baseDir: BaseDirectory.AppLocalData }); + * const fileInfo = await file.stat(); + * console.log(fileInfo.isFile); // true + * await file.close(); + * ``` + * + * @returns A promise resolving to the metadata of this file. + * @since 2.0.0 + */ + async stat(): Promise { + const res = await invoke('plugin:fs|fstat', { + rid: this.rid + }) + + return parseFileInfo(res) + } + + /** + * Truncates or extends this file, to reach the specified `len`. + * If `len` is not specified then the entire file contents are truncated. + * + * @example + * ```typescript + * import { open, BaseDirectory } from '@tauri-apps/plugin-fs'; + * + * // truncate the entire file + * const file = await open("my_file.txt", { read: true, write: true, create: true, baseDir: BaseDirectory.AppLocalData }); + * await file.truncate(); + * + * // truncate part of the file + * const file = await open("my_file.txt", { read: true, write: true, create: true, baseDir: BaseDirectory.AppLocalData }); + * await file.write(new TextEncoder().encode("Hello World")); + * await file.truncate(7); + * const data = new Uint8Array(32); + * await file.read(data); + * console.log(new TextDecoder().decode(data)); // Hello W + * await file.close(); + * ``` + * + * @param len The length the file is truncated or extended to, in bytes. When not provided the entire file contents are truncated. + * @since 2.0.0 + */ + async truncate(len?: number): Promise { + await invoke('plugin:fs|ftruncate', { + rid: this.rid, + len + }) + } + + /** + * Writes `data.byteLength` bytes from `data` to the underlying data stream. It + * resolves to the number of bytes written from `data` (`0` <= `n` <= + * `data.byteLength`) or reject with the error encountered that caused the + * write to stop early. `write()` must reject with a non-null error if + * would resolve to `n` < `data.byteLength`. `write()` must not modify the + * slice data, even temporarily. + * + * @example + * ```typescript + * import { open, write, BaseDirectory } from '@tauri-apps/plugin-fs'; + * const encoder = new TextEncoder(); + * const data = encoder.encode("Hello world"); + * const file = await open("bar.txt", { write: true, baseDir: BaseDirectory.AppLocalData }); + * const bytesWritten = await file.write(data); // 11 + * await file.close(); + * ``` + * + * @param data The bytes written to the file. + * @returns A promise resolving to the number of bytes written. + * @since 2.0.0 + */ + async write(data: Uint8Array): Promise { + return await invoke('plugin:fs|write', { + rid: this.rid, + data + }) + } +} + +/** + * Options for the `create` function, which creates or truncates a file. + * + * @since 2.0.0 + */ +interface CreateOptions { + /** Base directory for `path` */ + baseDir?: BaseDirectory +} + +/** + * Creates a file if none exists or truncates an existing file and resolves to + * an instance of {@linkcode FileHandle }. + * + * @example + * ```typescript + * import { create, BaseDirectory } from "@tauri-apps/plugin-fs" + * const file = await create("foo/bar.txt", { baseDir: BaseDirectory.AppConfig }); + * await file.write(new TextEncoder().encode("Hello world")); + * await file.close(); + * ``` + * + * @param path The path of the file, relative to `options.baseDir` when it is provided. + * @param options Options defining the base directory of `path`. + * @returns A promise resolving to the handle of the created file. + * @since 2.0.0 + */ +async function create( + path: string | URL, + options?: CreateOptions +): Promise { + if (path instanceof URL && path.protocol !== 'file:') { + throw new TypeError('Must be a file URL.') + } + + const rid = await invoke('plugin:fs|create', { + path: path instanceof URL ? path.toString() : path, + options + }) + + return new FileHandle(rid) +} + +/** + * Options for the `open` function, defining how the file is opened and which operations are allowed on it. + * + * @since 2.0.0 + */ +interface OpenOptions { + /** + * Sets the option for read access. This option, when `true`, means that the + * file should be read-able if opened. + */ + read?: boolean + /** + * Sets the option for write access. This option, when `true`, means that + * the file should be write-able if opened. If the file already exists, + * any write calls on it will overwrite its contents, by default without + * truncating it. + */ + write?: boolean + /** + * Sets the option for the append mode. This option, when `true`, means that + * writes will append to a file instead of overwriting previous contents. + * Note that setting `{ write: true, append: true }` has the same effect as + * setting only `{ append: true }`. + */ + append?: boolean + /** + * Sets the option for truncating a previous file. If a file is + * successfully opened with this option set it will truncate the file to `0` + * size if it already exists. The file must be opened with write access + * for truncate to work. + */ + truncate?: boolean + /** + * Sets the option to allow creating a new file, if one doesn't already + * exist at the specified path. Requires write or append access to be + * used. + */ + create?: boolean + /** + * Defaults to `false`. If set to `true`, no file, directory, or symlink is + * allowed to exist at the target location. Requires write or append + * access to be used. When createNew is set to `true`, create and truncate + * are ignored. + */ + createNew?: boolean + /** + * Permissions to use if creating the file (defaults to `0o666`, before + * the process's umask). + * Ignored on Windows. + */ + mode?: number + /** Base directory for `path` */ + baseDir?: BaseDirectory +} + +/** + * Open a file and resolve to an instance of {@linkcode FileHandle}. The + * file does not need to previously exist if using the `create` or `createNew` + * open options. It is the callers responsibility to close the file when finished + * with it. + * + * @example + * ```typescript + * import { open, BaseDirectory } from "@tauri-apps/plugin-fs" + * const file = await open("foo/bar.txt", { read: true, write: true, baseDir: BaseDirectory.AppLocalData }); + * // Do work with file + * await file.close(); + * ``` + * + * @param path The path of the file, relative to `options.baseDir` when it is provided. + * @param options Options defining the base directory of `path` and how the file is opened. + * @returns A promise resolving to the handle of the open file. + * @since 2.0.0 + */ +async function open( + path: string | URL, + options?: OpenOptions +): Promise { + if (path instanceof URL && path.protocol !== 'file:') { + throw new TypeError('Must be a file URL.') + } + + const rid = await invoke('plugin:fs|open', { + path: path instanceof URL ? path.toString() : path, + options + }) + + return new FileHandle(rid) +} + +/** + * Options for the `copyFile` function, defining the base directory of each path. + * + * @since 2.0.0 + */ +interface CopyFileOptions { + /** Base directory for `fromPath`. */ + fromPathBaseDir?: BaseDirectory + /** Base directory for `toPath`. */ + toPathBaseDir?: BaseDirectory +} + +/** + * Copies the contents and permissions of one file to another specified path, by default creating a new file if needed, else overwriting. + * @example + * ```typescript + * import { copyFile, BaseDirectory } from '@tauri-apps/plugin-fs'; + * await copyFile('app.conf', 'app.conf.bk', { fromPathBaseDir: BaseDirectory.AppConfig, toPathBaseDir: BaseDirectory.AppConfig }); + * ``` + * + * @param fromPath The path of the file to copy from. + * @param toPath The path of the file to copy to. + * @param options Options defining the base directory of each path. + * @since 2.0.0 + */ +async function copyFile( + fromPath: string | URL, + toPath: string | URL, + options?: CopyFileOptions +): Promise { + if ( + (fromPath instanceof URL && fromPath.protocol !== 'file:') + || (toPath instanceof URL && toPath.protocol !== 'file:') + ) { + throw new TypeError('Must be a file URL.') + } + + await invoke('plugin:fs|copy_file', { + fromPath: fromPath instanceof URL ? fromPath.toString() : fromPath, + toPath: toPath instanceof URL ? toPath.toString() : toPath, + options + }) +} + +/** + * Options for the `mkdir` function, which creates a directory. + * + * @since 2.0.0 + */ +interface MkdirOptions { + /** Permissions to use when creating the directory (defaults to `0o777`, before the process's umask). Ignored on Windows. */ + mode?: number + /** + * Defaults to `false`. If set to `true`, means that any intermediate directories will also be created (as with the shell command `mkdir -p`). + * */ + recursive?: boolean + /** Base directory for `path` */ + baseDir?: BaseDirectory +} + +/** + * Creates a new directory with the specified path. + * @example + * ```typescript + * import { mkdir, BaseDirectory } from '@tauri-apps/plugin-fs'; + * await mkdir('users', { baseDir: BaseDirectory.AppLocalData }); + * ``` + * + * @param path The path of the directory to create. + * @param options Options defining the base directory of `path`, the directory permissions and whether intermediate directories are created. + * @since 2.0.0 + */ +async function mkdir( + path: string | URL, + options?: MkdirOptions +): Promise { + if (path instanceof URL && path.protocol !== 'file:') { + throw new TypeError('Must be a file URL.') + } + + await invoke('plugin:fs|mkdir', { + path: path instanceof URL ? path.toString() : path, + options + }) +} + +/** + * Options for the `readDir` function, which lists the entries of a directory. + * + * @since 2.0.0 + */ +interface ReadDirOptions { + /** Base directory for `path` */ + baseDir?: BaseDirectory +} + +/** + * A disk entry which is either a file, a directory or a symlink. + * + * This is the result of the {@linkcode readDir}. + * + * @since 2.0.0 + */ +interface DirEntry { + /** The name of the entry (file name with extension or directory name). */ + name: string + /** Specifies whether this entry is a directory or not. */ + isDirectory: boolean + /** Specifies whether this entry is a file or not. */ + isFile: boolean + /** Specifies whether this entry is a symlink or not. */ + isSymlink: boolean +} + +/** + * Reads the directory given by path and returns an array of `DirEntry`. + * @example + * ```typescript + * import { readDir, BaseDirectory } from '@tauri-apps/plugin-fs'; + * import { join } from '@tauri-apps/api/path'; + * const dir = 'users'; + * const entries = await readDir(dir, { baseDir: BaseDirectory.AppLocalData }); + * await processEntriesRecursively(dir, entries); + * async function processEntriesRecursively(parent, entries) { + * for (const entry of entries) { + * console.log(`Entry: ${entry.name}`); + * if (entry.isDirectory) { + * const entryPath = await join(parent, entry.name); + * await processEntriesRecursively(entryPath, await readDir(entryPath, { baseDir: BaseDirectory.AppLocalData })); + * } + * } + * } + * ``` + * + * @param path The path of the directory to read. + * @param options Options defining the base directory of `path`. + * @returns A promise resolving to the list of entries in the directory. + * @since 2.0.0 + */ +async function readDir( + path: string | URL, + options?: ReadDirOptions +): Promise { + if (path instanceof URL && path.protocol !== 'file:') { + throw new TypeError('Must be a file URL.') + } + + return await invoke('plugin:fs|read_dir', { + path: path instanceof URL ? path.toString() : path, + options + }) +} + +/** + * Options for the functions that read a file, such as `readFile` and `readTextFile`. + * + * @since 2.0.0 + */ +interface ReadFileOptions { + /** Base directory for `path` */ + baseDir?: BaseDirectory + /** Text encoding to use when reading a text file. Defaults to 'utf-8'. */ + encoding?: string +} + +/** + * Reads and resolves to the entire contents of a file as an array of bytes. + * TextDecoder can be used to transform the bytes to string if required. + * @example + * ```typescript + * import { readFile, BaseDirectory } from '@tauri-apps/plugin-fs'; + * const contents = await readFile('avatar.png', { baseDir: BaseDirectory.Resource }); + * ``` + * + * @param path The path of the file to read. + * @param options Options defining the base directory of `path`. + * @returns A promise resolving to the contents of the file as bytes. + * @since 2.0.0 + */ +async function readFile( + path: string | URL, + options?: ReadFileOptions +): Promise> { + if (path instanceof URL && path.protocol !== 'file:') { + throw new TypeError('Must be a file URL.') + } + + const arr = await invoke('plugin:fs|read_file', { + path: path instanceof URL ? path.toString() : path, + options + }) + + return arr instanceof ArrayBuffer ? new Uint8Array(arr) : Uint8Array.from(arr) +} + +/** + * Reads and returns the entire contents of a file as a string using the specified encoding (default: UTF-8). + * @example + * ```typescript + * import { readTextFile, BaseDirectory } from '@tauri-apps/plugin-fs'; + * const contents = await readTextFile('app.conf', { baseDir: BaseDirectory.AppConfig }); + * ``` + * + * @param path The path of the file to read. + * @param options Options defining the base directory of `path` and the text encoding. + * @returns A promise resolving to the contents of the file as a string. + * @since 2.0.0 + */ +async function readTextFile( + path: string | URL, + options?: ReadFileOptions +): Promise { + if (path instanceof URL && path.protocol !== 'file:') { + throw new TypeError('Must be a file URL.') + } + + const arr = await invoke('plugin:fs|read_text_file', { + path: path instanceof URL ? path.toString() : path, + options + }) + + const bytes = arr instanceof ArrayBuffer ? arr : Uint8Array.from(arr) + + return new TextDecoder(options?.encoding ?? 'utf-8').decode(bytes) +} + +/** + * Returns an async {@linkcode AsyncIterableIterator} over the lines of a file, decoded using the specified encoding (default: UTF-8). + * @example + * ```typescript + * import { readTextFileLines, BaseDirectory } from '@tauri-apps/plugin-fs'; + * const lines = await readTextFileLines('app.conf', { baseDir: BaseDirectory.AppConfig }); + * for await (const line of lines) { + * console.log(line); + * } + * ``` + * You could also call {@linkcode AsyncIterableIterator.next} to advance the + * iterator so you can lazily read the next line whenever you want. + * + * @param path The path of the file to read. + * @param options Options defining the base directory of `path` and the text encoding. + * @returns A promise resolving to an iterator over the lines of the file. + * @since 2.0.0 + */ +async function readTextFileLines( + path: string | URL, + options?: ReadFileOptions +): Promise> { + if (path instanceof URL && path.protocol !== 'file:') { + throw new TypeError('Must be a file URL.') + } + + const pathStr = path instanceof URL ? path.toString() : path + + return await Promise.resolve({ + path: pathStr, + rid: null as number | null, + + async next(): Promise> { + const decoder = new TextDecoder(options?.encoding ?? 'utf-8') + + if (this.rid === null) { + // Use the normalized encoding label for options. + const encoding = decoder.encoding + + this.rid = await invoke('plugin:fs|read_text_file_lines', { + path: pathStr, + options: options != null ? { ...options, encoding } : undefined + }) + } + + const arr = await invoke( + 'plugin:fs|read_text_file_lines_next', + { rid: this.rid } + ) + + const bytes = + arr instanceof ArrayBuffer ? new Uint8Array(arr) : Uint8Array.from(arr) + + // Rust side will never return an empty array for this command and + // ensure there is at least one elements there. + // + // This is an optimization to include whether we finished iteration or not (1 or 0) + // at the end of returned array to avoid serialization overhead of separate values. + const done = bytes[bytes.byteLength - 1] === 1 + + if (done) { + // a full iteration is over, reset rid for next iteration + this.rid = null + return { value: null, done } + } + + const line = decoder.decode(bytes.slice(0, bytes.byteLength - 1)) + + return { + value: line, + done + } + }, + + [Symbol.asyncIterator](): AsyncIterableIterator { + return this + } + }) +} + +/** + * Options for the `remove` function, which deletes a file or a directory. + * + * @since 2.0.0 + */ +interface RemoveOptions { + /** Defaults to `false`. If set to `true`, path will be removed even if it's a non-empty directory. */ + recursive?: boolean + /** Base directory for `path` */ + baseDir?: BaseDirectory +} + +/** + * Removes the named file or directory. + * If the directory is not empty and the `recursive` option isn't set to true, the promise will be rejected. + * @example + * ```typescript + * import { remove, BaseDirectory } from '@tauri-apps/plugin-fs'; + * await remove('users/file.txt', { baseDir: BaseDirectory.AppLocalData }); + * await remove('users', { baseDir: BaseDirectory.AppLocalData }); + * ``` + * + * @param path The path of the file or directory to remove. + * @param options Options defining the base directory of `path` and whether directories are removed recursively. + * @since 2.0.0 + */ +async function remove( + path: string | URL, + options?: RemoveOptions +): Promise { + if (path instanceof URL && path.protocol !== 'file:') { + throw new TypeError('Must be a file URL.') + } + + await invoke('plugin:fs|remove', { + path: path instanceof URL ? path.toString() : path, + options + }) +} + +/** + * Options for the `rename` function, defining the base directory of each path. + * + * @since 2.0.0 + */ +interface RenameOptions { + /** Base directory for `oldPath`. */ + oldPathBaseDir?: BaseDirectory + /** Base directory for `newPath`. */ + newPathBaseDir?: BaseDirectory +} + +/** + * Renames (moves) oldpath to newpath. Paths may be files or directories. + * If newpath already exists and is not a directory, rename() replaces it. + * OS-specific restrictions may apply when oldpath and newpath are in different directories. + * + * On Unix, this operation does not follow symlinks at either path. + * + * @example + * ```typescript + * import { rename, BaseDirectory } from '@tauri-apps/plugin-fs'; + * await rename('avatar.png', 'deleted.png', { oldPathBaseDir: BaseDirectory.App, newPathBaseDir: BaseDirectory.AppLocalData }); + * ``` + * + * @param oldPath The path of the file or directory to rename. + * @param newPath The path the file or directory is renamed to. + * @param options Options defining the base directory of each path. + * @since 2.0.0 + */ +async function rename( + oldPath: string | URL, + newPath: string | URL, + options?: RenameOptions +): Promise { + if ( + (oldPath instanceof URL && oldPath.protocol !== 'file:') + || (newPath instanceof URL && newPath.protocol !== 'file:') + ) { + throw new TypeError('Must be a file URL.') + } + + await invoke('plugin:fs|rename', { + oldPath: oldPath instanceof URL ? oldPath.toString() : oldPath, + newPath: newPath instanceof URL ? newPath.toString() : newPath, + options + }) +} + +/** + * Options for the `stat` and `lstat` functions, which read the metadata of a path. + * + * @since 2.0.0 + */ +interface StatOptions { + /** Base directory for `path`. */ + baseDir?: BaseDirectory +} + +/** + * Resolves to a {@linkcode FileInfo} for the specified `path`. Will always + * follow symlinks but will reject if the symlink points to a path outside of the scope. + * + * @example + * ```typescript + * import { stat, BaseDirectory } from '@tauri-apps/plugin-fs'; + * const fileInfo = await stat("hello.txt", { baseDir: BaseDirectory.AppLocalData }); + * console.log(fileInfo.isFile); // true + * ``` + * + * @param path The path of the file or directory to inspect. + * @param options Options defining the base directory of `path`. + * @returns A promise resolving to the metadata of the file or directory. + * @since 2.0.0 + */ +async function stat( + path: string | URL, + options?: StatOptions +): Promise { + const res = await invoke('plugin:fs|stat', { + path: path instanceof URL ? path.toString() : path, + options + }) + + return parseFileInfo(res) +} + +/** + * Resolves to a {@linkcode FileInfo} for the specified `path`. If `path` is a + * symlink, information for the symlink will be returned instead of what it + * points to. + * + * @example + * ```typescript + * import { lstat, BaseDirectory } from '@tauri-apps/plugin-fs'; + * const fileInfo = await lstat("hello.txt", { baseDir: BaseDirectory.AppLocalData }); + * console.log(fileInfo.isFile); // true + * ``` + * + * @param path The path of the file, directory or symlink to inspect. + * @param options Options defining the base directory of `path`. + * @returns A promise resolving to the metadata of the path itself. + * @since 2.0.0 + */ +async function lstat( + path: string | URL, + options?: StatOptions +): Promise { + const res = await invoke('plugin:fs|lstat', { + path: path instanceof URL ? path.toString() : path, + options + }) + + return parseFileInfo(res) +} + +/** + * Options for the `truncate` function, which truncates or extends a file. + * + * @since 2.0.0 + */ +interface TruncateOptions { + /** Base directory for `path`. */ + baseDir?: BaseDirectory +} + +/** + * Truncates or extends the specified file, to reach the specified `len`. + * If `len` is `0` or not specified, then the entire file contents are truncated. + * + * @example + * ```typescript + * import { truncate, readTextFile, writeTextFile, BaseDirectory } from '@tauri-apps/plugin-fs'; + * // truncate the entire file + * await truncate("my_file.txt", 0, { baseDir: BaseDirectory.AppLocalData }); + * + * // truncate part of the file + * const filePath = "file.txt"; + * await writeTextFile(filePath, "Hello World", { baseDir: BaseDirectory.AppLocalData }); + * await truncate(filePath, 7, { baseDir: BaseDirectory.AppLocalData }); + * const data = await readTextFile(filePath, { baseDir: BaseDirectory.AppLocalData }); + * console.log(data); // "Hello W" + * ``` + * + * @param path The path of the file to truncate or extend. + * @param len The length the file is resized to, in bytes. Defaults to `0`. + * @param options Options defining the base directory of `path`. + * @since 2.0.0 + */ +async function truncate( + path: string | URL, + len?: number, + options?: TruncateOptions +): Promise { + if (path instanceof URL && path.protocol !== 'file:') { + throw new TypeError('Must be a file URL.') + } + + await invoke('plugin:fs|truncate', { + path: path instanceof URL ? path.toString() : path, + len, + options + }) +} + +/** + * Options for the `writeFile` and `writeTextFile` functions, defining how the file is opened before writing to it. + * + * @since 2.0.0 + */ +interface WriteFileOptions { + /** Defaults to `false`. If set to `true`, will append to a file instead of overwriting previous contents. */ + append?: boolean + /** Sets the option to allow creating a new file, if one doesn't already exist at the specified path (defaults to `true`). */ + create?: boolean + /** Sets the option to create a new file, failing if it already exists. */ + createNew?: boolean + /** File permissions. Ignored on Windows. */ + mode?: number + /** Base directory for `path` */ + baseDir?: BaseDirectory +} + +/** + * Write `data` to the given `path`, by default creating a new file if needed, else overwriting. + * @example + * ```typescript + * import { writeFile, BaseDirectory } from '@tauri-apps/plugin-fs'; + * + * let encoder = new TextEncoder(); + * let data = encoder.encode("Hello World"); + * await writeFile('file.txt', data, { baseDir: BaseDirectory.AppLocalData }); + * ``` + * + * @param path The path of the file to write to. + * @param data The bytes written to the file, either as a buffer or as a stream of chunks. + * @param options Options defining the base directory of `path` and how the file is opened. + * @since 2.0.0 + */ +async function writeFile( + path: string | URL, + data: Uint8Array | ReadableStream, + options?: WriteFileOptions +): Promise { + if (path instanceof URL && path.protocol !== 'file:') { + throw new TypeError('Must be a file URL.') + } + + if (data instanceof ReadableStream) { + const file = await open(path, { + read: false, + create: true, + write: true, + ...options + }) + const reader = data.getReader() + + try { + while (true) { + const { done, value } = await reader.read() + if (done) break + await file.write(value) + } + } finally { + reader.releaseLock() + await file.close() + } + } else { + await invoke('plugin:fs|write_file', data, { + headers: { + path: encodeURIComponent(path instanceof URL ? path.toString() : path), + options: JSON.stringify(options) + } + }) + } +} + +/** + * Writes UTF-8 string `data` to the given `path`, by default creating a new file if needed, else overwriting. + * + * @example + * ```typescript + * import { writeTextFile, BaseDirectory } from '@tauri-apps/plugin-fs'; + * + * await writeTextFile('file.txt', "Hello world", { baseDir: BaseDirectory.AppLocalData }); + * ``` + * + * @param path The path of the file to write to. + * @param data The UTF-8 string written to the file. + * @param options Options defining the base directory of `path` and how the file is opened. + * @since 2.0.0 + */ +async function writeTextFile( + path: string | URL, + data: string, + options?: WriteFileOptions +): Promise { + if (path instanceof URL && path.protocol !== 'file:') { + throw new TypeError('Must be a file URL.') + } + + const encoder = new TextEncoder() + + await invoke('plugin:fs|write_text_file', encoder.encode(data), { + headers: { + path: encodeURIComponent(path instanceof URL ? path.toString() : path), + options: JSON.stringify(options) + } + }) +} + +/** + * Options for the `exists` function, which checks whether a path exists. + * + * @since 2.0.0 + */ +interface ExistsOptions { + /** Base directory for `path`. */ + baseDir?: BaseDirectory +} + +/** + * Check if a path exists. + * @example + * ```typescript + * import { exists, BaseDirectory } from '@tauri-apps/plugin-fs'; + * // Check if the `$APPDATA/avatar.png` file exists + * await exists('avatar.png', { baseDir: BaseDirectory.AppData }); + * ``` + * + * @param path The path to check. + * @param options Options defining the base directory of `path`. + * @returns A promise resolving to `true` when the path exists, `false` otherwise. + * @since 2.0.0 + */ +async function exists( + path: string | URL, + options?: ExistsOptions +): Promise { + if (path instanceof URL && path.protocol !== 'file:') { + throw new TypeError('Must be a file URL.') + } + + return await invoke('plugin:fs|exists', { + path: path instanceof URL ? path.toString() : path, + options + }) +} + +/** + * Options for the `watchImmediate` function, which reports file system changes as they happen. + * + * @since 2.0.0 + */ +interface WatchOptions { + /** Watch a directory recursively */ + recursive?: boolean + /** Base directory for `path` */ + baseDir?: BaseDirectory +} + +/** + * Options for the `watch` function, which reports file system changes after a debounce delay. + * + * @since 2.0.0 + */ +interface DebouncedWatchOptions extends WatchOptions { + /** + * The debounce delay in milliseconds. Changes that happen within this + * window are grouped and reported together. Defaults to `2000`. + */ + delayMs?: number +} + +/** + * A file system change reported to the callback of `watch` or `watchImmediate`. + * + * @since 2.0.0 + */ +interface WatchEvent { + /** The kind of change that was detected. */ + type: WatchEventKind + /** The paths affected by the change. */ + paths: string[] + /** Additional attributes reported by the underlying file system watcher. */ + attrs: unknown +} + +/** + * The kind of file system change described by a `WatchEvent`. + * + * @since 2.0.0 + */ +type WatchEventKind = + | 'any' + | { access: WatchEventKindAccess } + | { create: WatchEventKindCreate } + | { modify: WatchEventKindModify } + | { remove: WatchEventKindRemove } + | 'other' + +/** + * Describes how a file or directory was accessed. + * + * @since 2.0.0 + */ +type WatchEventKindAccess = + | { kind: 'any' } + | { kind: 'close'; mode: 'any' | 'execute' | 'read' | 'write' | 'other' } + | { kind: 'open'; mode: 'any' | 'execute' | 'read' | 'write' | 'other' } + | { kind: 'other' } + +/** + * Describes which kind of entry was created. + * + * @since 2.0.0 + */ +type WatchEventKindCreate = + | { kind: 'any' } + | { kind: 'file' } + | { kind: 'folder' } + | { kind: 'other' } + +/** + * Describes what was modified on a file or directory. + * + * @since 2.0.0 + */ +type WatchEventKindModify = + | { kind: 'any' } + | { kind: 'data'; mode: 'any' | 'size' | 'content' | 'other' } + | { + kind: 'metadata' + mode: + | 'any' + | 'access-time' + | 'write-time' + | 'permissions' + | 'ownership' + | 'extended' + | 'other' + } + | { kind: 'rename'; mode: 'any' | 'to' | 'from' | 'both' | 'other' } + | { kind: 'other' } + +/** + * Describes which kind of entry was removed. + * + * @since 2.0.0 + */ +type WatchEventKindRemove = + | { kind: 'any' } + | { kind: 'file' } + | { kind: 'folder' } + | { kind: 'other' } + +// TODO: Remove this in v3, return `Watcher` instead +/** + * Stops watching the paths it was created for. Returned by `watch` and `watchImmediate`. + * + * @since 2.0.0 + */ +type UnwatchFn = () => void + +class Watcher extends Resource {} + +async function watchInternal( + paths: string | string[] | URL | URL[], + cb: (event: WatchEvent) => void, + options: DebouncedWatchOptions +): Promise { + const watchPaths = Array.isArray(paths) ? paths : [paths] + + for (const path of watchPaths) { + if (path instanceof URL && path.protocol !== 'file:') { + throw new TypeError('Must be a file URL.') + } + } + + const onEvent = new Channel() + onEvent.onmessage = cb + + const rid: number = await invoke('plugin:fs|watch', { + paths: watchPaths.map((p) => (p instanceof URL ? p.toString() : p)), + options, + onEvent + }) + + const watcher = new Watcher(rid) + + return () => { + void watcher.close() + } +} + +// TODO: Return `Watcher` instead in v3 +/** + * Watch changes (after a delay) on files or directories. + * + * Events that happen within the `delayMs` window are grouped and delivered in a single callback call. + * Requires the `watch` Cargo feature of the Rust plugin to be enabled. + * + * @example + * ```typescript + * import { watch, BaseDirectory } from '@tauri-apps/plugin-fs'; + * + * const unwatch = await watch( + * 'app.conf', + * (event) => console.log(event.type, event.paths), + * { baseDir: BaseDirectory.AppConfig, delayMs: 500 } + * ); + * + * // stop watching when you are done + * unwatch(); + * ``` + * + * @param paths The path or list of paths to watch. Each path can be a string or a `file://` URL. + * @param cb The callback executed for each batch of file system changes. + * @param options Options defining the base directory of the paths, the debounce delay and whether directories are watched recursively. + * @returns A promise resolving to a function that stops watching the given paths. + * @since 2.0.0 + */ +async function watch( + paths: string | string[] | URL | URL[], + cb: (event: WatchEvent) => void, + options?: DebouncedWatchOptions +): Promise { + return await watchInternal(paths, cb, { + delayMs: 2000, + ...options + }) +} + +// TODO: Return `Watcher` instead in v3 +/** + * Watch changes on files or directories. + * + * Unlike `watch`, changes are reported as soon as they are detected, without a debounce delay. + * Requires the `watch` Cargo feature of the Rust plugin to be enabled. + * + * @example + * ```typescript + * import { watchImmediate, BaseDirectory } from '@tauri-apps/plugin-fs'; + * + * const unwatch = await watchImmediate( + * 'logs', + * (event) => console.log(event.type, event.paths), + * { baseDir: BaseDirectory.AppLog, recursive: true } + * ); + * + * // stop watching when you are done + * unwatch(); + * ``` + * + * @param paths The path or list of paths to watch. Each path can be a string or a `file://` URL. + * @param cb The callback executed for each file system change. + * @param options Options defining the base directory of the paths and whether directories are watched recursively. + * @returns A promise resolving to a function that stops watching the given paths. + * @since 2.0.0 + */ +async function watchImmediate( + paths: string | string[] | URL | URL[], + cb: (event: WatchEvent) => void, + options?: WatchOptions +): Promise { + return await watchInternal(paths, cb, { + ...options, + delayMs: undefined + }) +} + +/** + * Get the size of a file or directory. For files, the `stat` functions can be used as well. + * + * If `path` is a directory, this function will recursively iterate over every file and every directory inside of `path` and therefore will be very time consuming if used on larger directories. + * + * @example + * ```typescript + * import { size, BaseDirectory } from '@tauri-apps/plugin-fs'; + * // Get the size of the `$APPDATA/tauri` directory. + * const dirSize = await size('tauri', { baseDir: BaseDirectory.AppData }); + * console.log(dirSize); // 1024 + * ``` + * + * @param path The path of the file or directory to measure. + * @returns A promise resolving to the size in bytes. + * @since 2.1.0 + */ +async function size(path: string | URL): Promise { + if (path instanceof URL && path.protocol !== 'file:') { + throw new TypeError('Must be a file URL.') + } + + return await invoke('plugin:fs|size', { + path: path instanceof URL ? path.toString() : path + }) +} + +/** + * Starts accessing a security-scoped resource for the given file URL. + * This should be called when you're accessing a file that was opened + * using a security-scoped URL (e.g., from a file picker). + * + * Note that accessing security-scoped resources is automatically managed by the plugin on iOS, so you don't need to call this function + * unless you want to manage the scope manually. + * + * You must call {@linkcode stopAccessingSecurityScopedResource} when you're done accessing the resource. + * + * #### Platform-specific + * + * - **iOS:** Starts accessing the security-scoped resource. + * - **Other platforms:** does nothing. + * + * @example + * ```typescript + * import { startAccessingSecurityScopedResource } from '@tauri-apps/plugin-fs'; + * + * const filePath = 'file:///path/to/file.txt'; + * await startAccessingSecurityScopedResource(filePath); + * // ... use the resource ... + * ``` + * + * @param path The path or `file://` URL of the resource to start accessing. + * @since 2.5.0 + */ +async function startAccessingSecurityScopedResource( + path: string | URL +): Promise { + if (path instanceof URL && path.protocol !== 'file:') { + throw new TypeError('Must be a file URL.') + } + + await invoke('plugin:fs|start_accessing_security_scoped_resource', { + path: path instanceof URL ? path.toString() : path + }) +} + +/** + * Stops accessing a security-scoped resource for the given file URL. + * This should be called when you're done accessing a file that was opened + * using a security-scoped URL (e.g., from a file picker) when using manual tracking via {@linkcode startAccessingSecurityScopedResource}. + * + * #### Platform-specific + * + * - **iOS:** Stops accessing the security-scoped resource. + * - **Other platforms:** does nothing. + * + * @example + * ```typescript + * import { stopAccessingSecurityScopedResource } from '@tauri-apps/plugin-fs'; + * + * const filePath = 'file:///path/to/file.txt'; + * await startAccessingSecurityScopedResource(filePath); + * // ... use the resource ... + * // when you're done with the resource: + * await stopAccessingSecurityScopedResource(filePath); + * ``` + * + * @param path The path or `file://` URL of the resource to stop accessing. + * @since 2.5.0 + */ +async function stopAccessingSecurityScopedResource( + path: string | URL +): Promise { + if (path instanceof URL && path.protocol !== 'file:') { + throw new TypeError('Must be a file URL.') + } + + await invoke('plugin:fs|stop_accessing_security_scoped_resource', { + path: path instanceof URL ? path.toString() : path + }) +} + +export type { + CreateOptions, + OpenOptions, + CopyFileOptions, + MkdirOptions, + DirEntry, + ReadDirOptions, + ReadFileOptions, + RemoveOptions, + RenameOptions, + StatOptions, + TruncateOptions, + WriteFileOptions, + ExistsOptions, + FileInfo, + WatchOptions, + DebouncedWatchOptions, + WatchEvent, + WatchEventKind, + WatchEventKindAccess, + WatchEventKindCreate, + WatchEventKindModify, + WatchEventKindRemove, + UnwatchFn +} + +export { + BaseDirectory, + FileHandle, + create, + open, + copyFile, + mkdir, + readDir, + readFile, + readTextFile, + readTextFileLines, + remove, + rename, + SeekMode, + stat, + lstat, + truncate, + writeFile, + writeTextFile, + exists, + watch, + watchImmediate, + size, + startAccessingSecurityScopedResource, + stopAccessingSecurityScopedResource +} diff --git a/plugins/fs/package.json b/plugins/fs/package.json new file mode 100644 index 0000000..7fb9b3e --- /dev/null +++ b/plugins/fs/package.json @@ -0,0 +1,30 @@ +{ + "name": "@tauri-apps/plugin-fs", + "version": "2.5.2", + "description": "Access the file system.", + "license": "MIT OR Apache-2.0", + "authors": [ + "Tauri Programme within The Commons Conservancy" + ], + "repository": "https://github.com/tauri-apps/plugins-workspace", + "type": "module", + "types": "./dist-js/index.d.ts", + "main": "./dist-js/index.cjs", + "module": "./dist-js/index.js", + "exports": { + "types": "./dist-js/index.d.ts", + "import": "./dist-js/index.js", + "require": "./dist-js/index.cjs" + }, + "scripts": { + "build": "rollup -c" + }, + "files": [ + "dist-js", + "README.md", + "LICENSE" + ], + "dependencies": { + "@tauri-apps/api": "^2.11.0" + } +} diff --git a/plugins/fs/permissions/app.toml b/plugins/fs/permissions/app.toml new file mode 100644 index 0000000..aeb0521 --- /dev/null +++ b/plugins/fs/permissions/app.toml @@ -0,0 +1,114 @@ +"$schema" = "schemas/schema.json" + +# Scopes Section +# This section contains scopes, which define file level access + +[[permission]] +identifier = "scope-app-recursive" +description = "This scope permits recursive access to the complete application folders, including sub directories and files." + +[[permission.scope.allow]] +path = "$APPCONFIG" +[[permission.scope.allow]] +path = "$APPCONFIG/**" + +[[permission.scope.allow]] +path = "$APPDATA" +[[permission.scope.allow]] +path = "$APPDATA/**" + +[[permission.scope.allow]] +path = "$APPLOCALDATA" +[[permission.scope.allow]] +path = "$APPLOCALDATA/**" + +[[permission.scope.allow]] +path = "$APPCACHE" +[[permission.scope.allow]] +path = "$APPCACHE/**" + +[[permission.scope.allow]] +path = "$APPLOG" +[[permission.scope.allow]] +path = "$APPLOG/**" + +[[permission]] +identifier = "scope-app" +description = "This scope permits access to all files and list content of top level directories in the application folders." + +[[permission.scope.allow]] +path = "$APPCONFIG" +[[permission.scope.allow]] +path = "$APPCONFIG/*" + +[[permission.scope.allow]] +path = "$APPDATA" +[[permission.scope.allow]] +path = "$APPDATA/*" + +[[permission.scope.allow]] +path = "$APPLOCALDATA" +[[permission.scope.allow]] +path = "$APPLOCALDATA/*" + +[[permission.scope.allow]] +path = "$APPCACHE" +[[permission.scope.allow]] +path = "$APPCACHE/*" + +[[permission.scope.allow]] +path = "$APPLOG" +[[permission.scope.allow]] +path = "$APPLOG/*" + +[[permission]] +identifier = "scope-app-index" +description = "This scope permits to list all files and folders in the application directories." + +[[permission.scope.allow]] +path = "$APPCONFIG" + +[[permission.scope.allow]] +path = "$APPDATA" + +[[permission.scope.allow]] +path = "$APPLOCALDATA" + +[[permission.scope.allow]] +path = "$APPCACHE" + +[[permission.scope.allow]] +path = "$APPLOG" + +# Sets Section +# This section combines the scope elements with enablement of commands + +[[set]] +identifier = "allow-app-read-recursive" +description = "This allows full recursive read access to the complete application folders, files and subdirectories." +permissions = ["read-all", "scope-app-recursive"] + +[[set]] +identifier = "allow-app-write-recursive" +description = "This allows full recursive write access to the complete application folders, files and subdirectories." +permissions = ["write-all", "scope-app-recursive"] + +[[set]] +identifier = "allow-app-read" +description = "This allows non-recursive read access to the application folders." +permissions = ["read-all", "scope-app"] + +[[set]] +identifier = "allow-app-write" +description = "This allows non-recursive write access to the application folders." +permissions = ["write-all", "scope-app"] + +[[set]] +identifier = "allow-app-meta-recursive" +description = "This allows full recursive read access to metadata of the application folders, including file listing and statistics." +permissions = ["read-meta", "scope-app-recursive"] + +[[set]] +identifier = "allow-app-meta" +description = "This allows non-recursive read access to metadata of the application folders, including file listing and statistics." +permissions = ["read-meta", "scope-app-index"] diff --git a/plugins/fs/permissions/autogenerated/base-directories/appcache.toml b/plugins/fs/permissions/autogenerated/base-directories/appcache.toml new file mode 100644 index 0000000..50e19ef --- /dev/null +++ b/plugins/fs/permissions/autogenerated/base-directories/appcache.toml @@ -0,0 +1,82 @@ +# Automatically generated - DO NOT EDIT! + +"$schema" = "../../schemas/schema.json" + +# Scopes Section +# This section contains scopes, which define file level access + +[[permission]] +identifier = "scope-appcache-recursive" +description = "This scope permits recursive access to the complete `$APPCACHE` folder, including sub directories and files." + +[[permission.scope.allow]] +path = "$APPCACHE" +[[permission.scope.allow]] +path = "$APPCACHE/**" + +[[permission]] +identifier = "scope-appcache" +description = "This scope permits access to all files and list content of top level directories in the `$APPCACHE` folder." + +[[permission.scope.allow]] +path = "$APPCACHE" +[[permission.scope.allow]] +path = "$APPCACHE/*" + +[[permission]] +identifier = "scope-appcache-index" +description = "This scope permits to list all files and folders in the `$APPCACHE`folder." + +[[permission.scope.allow]] +path = "$APPCACHE" + +# Sets Section +# This section combines the scope elements with enablement of commands + +[[set]] +identifier = "allow-appcache-read-recursive" +description = "This allows full recursive read access to the complete `$APPCACHE` folder, files and subdirectories." +permissions = [ + "read-all", + "scope-appcache-recursive" +] + +[[set]] +identifier = "allow-appcache-write-recursive" +description = "This allows full recursive write access to the complete `$APPCACHE` folder, files and subdirectories." +permissions = [ + "write-all", + "scope-appcache-recursive" +] + +[[set]] +identifier = "allow-appcache-read" +description = "This allows non-recursive read access to the `$APPCACHE` folder." +permissions = [ + "read-all", + "scope-appcache" +] + +[[set]] +identifier = "allow-appcache-write" +description = "This allows non-recursive write access to the `$APPCACHE` folder." +permissions = [ + "write-all", + "scope-appcache" +] + +[[set]] +identifier = "allow-appcache-meta-recursive" +description = "This allows full recursive read access to metadata of the `$APPCACHE` folder, including file listing and statistics." +permissions = [ + "read-meta", + "scope-appcache-recursive" +] + +[[set]] +identifier = "allow-appcache-meta" +description = "This allows non-recursive read access to metadata of the `$APPCACHE` folder, including file listing and statistics." +permissions = [ + "read-meta", + "scope-appcache-index" +] \ No newline at end of file diff --git a/plugins/fs/permissions/autogenerated/base-directories/appconfig.toml b/plugins/fs/permissions/autogenerated/base-directories/appconfig.toml new file mode 100644 index 0000000..ab13695 --- /dev/null +++ b/plugins/fs/permissions/autogenerated/base-directories/appconfig.toml @@ -0,0 +1,82 @@ +# Automatically generated - DO NOT EDIT! + +"$schema" = "../../schemas/schema.json" + +# Scopes Section +# This section contains scopes, which define file level access + +[[permission]] +identifier = "scope-appconfig-recursive" +description = "This scope permits recursive access to the complete `$APPCONFIG` folder, including sub directories and files." + +[[permission.scope.allow]] +path = "$APPCONFIG" +[[permission.scope.allow]] +path = "$APPCONFIG/**" + +[[permission]] +identifier = "scope-appconfig" +description = "This scope permits access to all files and list content of top level directories in the `$APPCONFIG` folder." + +[[permission.scope.allow]] +path = "$APPCONFIG" +[[permission.scope.allow]] +path = "$APPCONFIG/*" + +[[permission]] +identifier = "scope-appconfig-index" +description = "This scope permits to list all files and folders in the `$APPCONFIG`folder." + +[[permission.scope.allow]] +path = "$APPCONFIG" + +# Sets Section +# This section combines the scope elements with enablement of commands + +[[set]] +identifier = "allow-appconfig-read-recursive" +description = "This allows full recursive read access to the complete `$APPCONFIG` folder, files and subdirectories." +permissions = [ + "read-all", + "scope-appconfig-recursive" +] + +[[set]] +identifier = "allow-appconfig-write-recursive" +description = "This allows full recursive write access to the complete `$APPCONFIG` folder, files and subdirectories." +permissions = [ + "write-all", + "scope-appconfig-recursive" +] + +[[set]] +identifier = "allow-appconfig-read" +description = "This allows non-recursive read access to the `$APPCONFIG` folder." +permissions = [ + "read-all", + "scope-appconfig" +] + +[[set]] +identifier = "allow-appconfig-write" +description = "This allows non-recursive write access to the `$APPCONFIG` folder." +permissions = [ + "write-all", + "scope-appconfig" +] + +[[set]] +identifier = "allow-appconfig-meta-recursive" +description = "This allows full recursive read access to metadata of the `$APPCONFIG` folder, including file listing and statistics." +permissions = [ + "read-meta", + "scope-appconfig-recursive" +] + +[[set]] +identifier = "allow-appconfig-meta" +description = "This allows non-recursive read access to metadata of the `$APPCONFIG` folder, including file listing and statistics." +permissions = [ + "read-meta", + "scope-appconfig-index" +] \ No newline at end of file diff --git a/plugins/fs/permissions/autogenerated/base-directories/appdata.toml b/plugins/fs/permissions/autogenerated/base-directories/appdata.toml new file mode 100644 index 0000000..1b0931e --- /dev/null +++ b/plugins/fs/permissions/autogenerated/base-directories/appdata.toml @@ -0,0 +1,82 @@ +# Automatically generated - DO NOT EDIT! + +"$schema" = "../../schemas/schema.json" + +# Scopes Section +# This section contains scopes, which define file level access + +[[permission]] +identifier = "scope-appdata-recursive" +description = "This scope permits recursive access to the complete `$APPDATA` folder, including sub directories and files." + +[[permission.scope.allow]] +path = "$APPDATA" +[[permission.scope.allow]] +path = "$APPDATA/**" + +[[permission]] +identifier = "scope-appdata" +description = "This scope permits access to all files and list content of top level directories in the `$APPDATA` folder." + +[[permission.scope.allow]] +path = "$APPDATA" +[[permission.scope.allow]] +path = "$APPDATA/*" + +[[permission]] +identifier = "scope-appdata-index" +description = "This scope permits to list all files and folders in the `$APPDATA`folder." + +[[permission.scope.allow]] +path = "$APPDATA" + +# Sets Section +# This section combines the scope elements with enablement of commands + +[[set]] +identifier = "allow-appdata-read-recursive" +description = "This allows full recursive read access to the complete `$APPDATA` folder, files and subdirectories." +permissions = [ + "read-all", + "scope-appdata-recursive" +] + +[[set]] +identifier = "allow-appdata-write-recursive" +description = "This allows full recursive write access to the complete `$APPDATA` folder, files and subdirectories." +permissions = [ + "write-all", + "scope-appdata-recursive" +] + +[[set]] +identifier = "allow-appdata-read" +description = "This allows non-recursive read access to the `$APPDATA` folder." +permissions = [ + "read-all", + "scope-appdata" +] + +[[set]] +identifier = "allow-appdata-write" +description = "This allows non-recursive write access to the `$APPDATA` folder." +permissions = [ + "write-all", + "scope-appdata" +] + +[[set]] +identifier = "allow-appdata-meta-recursive" +description = "This allows full recursive read access to metadata of the `$APPDATA` folder, including file listing and statistics." +permissions = [ + "read-meta", + "scope-appdata-recursive" +] + +[[set]] +identifier = "allow-appdata-meta" +description = "This allows non-recursive read access to metadata of the `$APPDATA` folder, including file listing and statistics." +permissions = [ + "read-meta", + "scope-appdata-index" +] \ No newline at end of file diff --git a/plugins/fs/permissions/autogenerated/base-directories/applocaldata.toml b/plugins/fs/permissions/autogenerated/base-directories/applocaldata.toml new file mode 100644 index 0000000..a6e38a3 --- /dev/null +++ b/plugins/fs/permissions/autogenerated/base-directories/applocaldata.toml @@ -0,0 +1,82 @@ +# Automatically generated - DO NOT EDIT! + +"$schema" = "../../schemas/schema.json" + +# Scopes Section +# This section contains scopes, which define file level access + +[[permission]] +identifier = "scope-applocaldata-recursive" +description = "This scope permits recursive access to the complete `$APPLOCALDATA` folder, including sub directories and files." + +[[permission.scope.allow]] +path = "$APPLOCALDATA" +[[permission.scope.allow]] +path = "$APPLOCALDATA/**" + +[[permission]] +identifier = "scope-applocaldata" +description = "This scope permits access to all files and list content of top level directories in the `$APPLOCALDATA` folder." + +[[permission.scope.allow]] +path = "$APPLOCALDATA" +[[permission.scope.allow]] +path = "$APPLOCALDATA/*" + +[[permission]] +identifier = "scope-applocaldata-index" +description = "This scope permits to list all files and folders in the `$APPLOCALDATA`folder." + +[[permission.scope.allow]] +path = "$APPLOCALDATA" + +# Sets Section +# This section combines the scope elements with enablement of commands + +[[set]] +identifier = "allow-applocaldata-read-recursive" +description = "This allows full recursive read access to the complete `$APPLOCALDATA` folder, files and subdirectories." +permissions = [ + "read-all", + "scope-applocaldata-recursive" +] + +[[set]] +identifier = "allow-applocaldata-write-recursive" +description = "This allows full recursive write access to the complete `$APPLOCALDATA` folder, files and subdirectories." +permissions = [ + "write-all", + "scope-applocaldata-recursive" +] + +[[set]] +identifier = "allow-applocaldata-read" +description = "This allows non-recursive read access to the `$APPLOCALDATA` folder." +permissions = [ + "read-all", + "scope-applocaldata" +] + +[[set]] +identifier = "allow-applocaldata-write" +description = "This allows non-recursive write access to the `$APPLOCALDATA` folder." +permissions = [ + "write-all", + "scope-applocaldata" +] + +[[set]] +identifier = "allow-applocaldata-meta-recursive" +description = "This allows full recursive read access to metadata of the `$APPLOCALDATA` folder, including file listing and statistics." +permissions = [ + "read-meta", + "scope-applocaldata-recursive" +] + +[[set]] +identifier = "allow-applocaldata-meta" +description = "This allows non-recursive read access to metadata of the `$APPLOCALDATA` folder, including file listing and statistics." +permissions = [ + "read-meta", + "scope-applocaldata-index" +] \ No newline at end of file diff --git a/plugins/fs/permissions/autogenerated/base-directories/applog.toml b/plugins/fs/permissions/autogenerated/base-directories/applog.toml new file mode 100644 index 0000000..a979ce7 --- /dev/null +++ b/plugins/fs/permissions/autogenerated/base-directories/applog.toml @@ -0,0 +1,82 @@ +# Automatically generated - DO NOT EDIT! + +"$schema" = "../../schemas/schema.json" + +# Scopes Section +# This section contains scopes, which define file level access + +[[permission]] +identifier = "scope-applog-recursive" +description = "This scope permits recursive access to the complete `$APPLOG` folder, including sub directories and files." + +[[permission.scope.allow]] +path = "$APPLOG" +[[permission.scope.allow]] +path = "$APPLOG/**" + +[[permission]] +identifier = "scope-applog" +description = "This scope permits access to all files and list content of top level directories in the `$APPLOG` folder." + +[[permission.scope.allow]] +path = "$APPLOG" +[[permission.scope.allow]] +path = "$APPLOG/*" + +[[permission]] +identifier = "scope-applog-index" +description = "This scope permits to list all files and folders in the `$APPLOG`folder." + +[[permission.scope.allow]] +path = "$APPLOG" + +# Sets Section +# This section combines the scope elements with enablement of commands + +[[set]] +identifier = "allow-applog-read-recursive" +description = "This allows full recursive read access to the complete `$APPLOG` folder, files and subdirectories." +permissions = [ + "read-all", + "scope-applog-recursive" +] + +[[set]] +identifier = "allow-applog-write-recursive" +description = "This allows full recursive write access to the complete `$APPLOG` folder, files and subdirectories." +permissions = [ + "write-all", + "scope-applog-recursive" +] + +[[set]] +identifier = "allow-applog-read" +description = "This allows non-recursive read access to the `$APPLOG` folder." +permissions = [ + "read-all", + "scope-applog" +] + +[[set]] +identifier = "allow-applog-write" +description = "This allows non-recursive write access to the `$APPLOG` folder." +permissions = [ + "write-all", + "scope-applog" +] + +[[set]] +identifier = "allow-applog-meta-recursive" +description = "This allows full recursive read access to metadata of the `$APPLOG` folder, including file listing and statistics." +permissions = [ + "read-meta", + "scope-applog-recursive" +] + +[[set]] +identifier = "allow-applog-meta" +description = "This allows non-recursive read access to metadata of the `$APPLOG` folder, including file listing and statistics." +permissions = [ + "read-meta", + "scope-applog-index" +] \ No newline at end of file diff --git a/plugins/fs/permissions/autogenerated/base-directories/audio.toml b/plugins/fs/permissions/autogenerated/base-directories/audio.toml new file mode 100644 index 0000000..d66d68a --- /dev/null +++ b/plugins/fs/permissions/autogenerated/base-directories/audio.toml @@ -0,0 +1,82 @@ +# Automatically generated - DO NOT EDIT! + +"$schema" = "../../schemas/schema.json" + +# Scopes Section +# This section contains scopes, which define file level access + +[[permission]] +identifier = "scope-audio-recursive" +description = "This scope permits recursive access to the complete `$AUDIO` folder, including sub directories and files." + +[[permission.scope.allow]] +path = "$AUDIO" +[[permission.scope.allow]] +path = "$AUDIO/**" + +[[permission]] +identifier = "scope-audio" +description = "This scope permits access to all files and list content of top level directories in the `$AUDIO` folder." + +[[permission.scope.allow]] +path = "$AUDIO" +[[permission.scope.allow]] +path = "$AUDIO/*" + +[[permission]] +identifier = "scope-audio-index" +description = "This scope permits to list all files and folders in the `$AUDIO`folder." + +[[permission.scope.allow]] +path = "$AUDIO" + +# Sets Section +# This section combines the scope elements with enablement of commands + +[[set]] +identifier = "allow-audio-read-recursive" +description = "This allows full recursive read access to the complete `$AUDIO` folder, files and subdirectories." +permissions = [ + "read-all", + "scope-audio-recursive" +] + +[[set]] +identifier = "allow-audio-write-recursive" +description = "This allows full recursive write access to the complete `$AUDIO` folder, files and subdirectories." +permissions = [ + "write-all", + "scope-audio-recursive" +] + +[[set]] +identifier = "allow-audio-read" +description = "This allows non-recursive read access to the `$AUDIO` folder." +permissions = [ + "read-all", + "scope-audio" +] + +[[set]] +identifier = "allow-audio-write" +description = "This allows non-recursive write access to the `$AUDIO` folder." +permissions = [ + "write-all", + "scope-audio" +] + +[[set]] +identifier = "allow-audio-meta-recursive" +description = "This allows full recursive read access to metadata of the `$AUDIO` folder, including file listing and statistics." +permissions = [ + "read-meta", + "scope-audio-recursive" +] + +[[set]] +identifier = "allow-audio-meta" +description = "This allows non-recursive read access to metadata of the `$AUDIO` folder, including file listing and statistics." +permissions = [ + "read-meta", + "scope-audio-index" +] \ No newline at end of file diff --git a/plugins/fs/permissions/autogenerated/base-directories/cache.toml b/plugins/fs/permissions/autogenerated/base-directories/cache.toml new file mode 100644 index 0000000..814319e --- /dev/null +++ b/plugins/fs/permissions/autogenerated/base-directories/cache.toml @@ -0,0 +1,82 @@ +# Automatically generated - DO NOT EDIT! + +"$schema" = "../../schemas/schema.json" + +# Scopes Section +# This section contains scopes, which define file level access + +[[permission]] +identifier = "scope-cache-recursive" +description = "This scope permits recursive access to the complete `$CACHE` folder, including sub directories and files." + +[[permission.scope.allow]] +path = "$CACHE" +[[permission.scope.allow]] +path = "$CACHE/**" + +[[permission]] +identifier = "scope-cache" +description = "This scope permits access to all files and list content of top level directories in the `$CACHE` folder." + +[[permission.scope.allow]] +path = "$CACHE" +[[permission.scope.allow]] +path = "$CACHE/*" + +[[permission]] +identifier = "scope-cache-index" +description = "This scope permits to list all files and folders in the `$CACHE`folder." + +[[permission.scope.allow]] +path = "$CACHE" + +# Sets Section +# This section combines the scope elements with enablement of commands + +[[set]] +identifier = "allow-cache-read-recursive" +description = "This allows full recursive read access to the complete `$CACHE` folder, files and subdirectories." +permissions = [ + "read-all", + "scope-cache-recursive" +] + +[[set]] +identifier = "allow-cache-write-recursive" +description = "This allows full recursive write access to the complete `$CACHE` folder, files and subdirectories." +permissions = [ + "write-all", + "scope-cache-recursive" +] + +[[set]] +identifier = "allow-cache-read" +description = "This allows non-recursive read access to the `$CACHE` folder." +permissions = [ + "read-all", + "scope-cache" +] + +[[set]] +identifier = "allow-cache-write" +description = "This allows non-recursive write access to the `$CACHE` folder." +permissions = [ + "write-all", + "scope-cache" +] + +[[set]] +identifier = "allow-cache-meta-recursive" +description = "This allows full recursive read access to metadata of the `$CACHE` folder, including file listing and statistics." +permissions = [ + "read-meta", + "scope-cache-recursive" +] + +[[set]] +identifier = "allow-cache-meta" +description = "This allows non-recursive read access to metadata of the `$CACHE` folder, including file listing and statistics." +permissions = [ + "read-meta", + "scope-cache-index" +] \ No newline at end of file diff --git a/plugins/fs/permissions/autogenerated/base-directories/config.toml b/plugins/fs/permissions/autogenerated/base-directories/config.toml new file mode 100644 index 0000000..5922104 --- /dev/null +++ b/plugins/fs/permissions/autogenerated/base-directories/config.toml @@ -0,0 +1,82 @@ +# Automatically generated - DO NOT EDIT! + +"$schema" = "../../schemas/schema.json" + +# Scopes Section +# This section contains scopes, which define file level access + +[[permission]] +identifier = "scope-config-recursive" +description = "This scope permits recursive access to the complete `$CONFIG` folder, including sub directories and files." + +[[permission.scope.allow]] +path = "$CONFIG" +[[permission.scope.allow]] +path = "$CONFIG/**" + +[[permission]] +identifier = "scope-config" +description = "This scope permits access to all files and list content of top level directories in the `$CONFIG` folder." + +[[permission.scope.allow]] +path = "$CONFIG" +[[permission.scope.allow]] +path = "$CONFIG/*" + +[[permission]] +identifier = "scope-config-index" +description = "This scope permits to list all files and folders in the `$CONFIG`folder." + +[[permission.scope.allow]] +path = "$CONFIG" + +# Sets Section +# This section combines the scope elements with enablement of commands + +[[set]] +identifier = "allow-config-read-recursive" +description = "This allows full recursive read access to the complete `$CONFIG` folder, files and subdirectories." +permissions = [ + "read-all", + "scope-config-recursive" +] + +[[set]] +identifier = "allow-config-write-recursive" +description = "This allows full recursive write access to the complete `$CONFIG` folder, files and subdirectories." +permissions = [ + "write-all", + "scope-config-recursive" +] + +[[set]] +identifier = "allow-config-read" +description = "This allows non-recursive read access to the `$CONFIG` folder." +permissions = [ + "read-all", + "scope-config" +] + +[[set]] +identifier = "allow-config-write" +description = "This allows non-recursive write access to the `$CONFIG` folder." +permissions = [ + "write-all", + "scope-config" +] + +[[set]] +identifier = "allow-config-meta-recursive" +description = "This allows full recursive read access to metadata of the `$CONFIG` folder, including file listing and statistics." +permissions = [ + "read-meta", + "scope-config-recursive" +] + +[[set]] +identifier = "allow-config-meta" +description = "This allows non-recursive read access to metadata of the `$CONFIG` folder, including file listing and statistics." +permissions = [ + "read-meta", + "scope-config-index" +] \ No newline at end of file diff --git a/plugins/fs/permissions/autogenerated/base-directories/data.toml b/plugins/fs/permissions/autogenerated/base-directories/data.toml new file mode 100644 index 0000000..a8428ca --- /dev/null +++ b/plugins/fs/permissions/autogenerated/base-directories/data.toml @@ -0,0 +1,82 @@ +# Automatically generated - DO NOT EDIT! + +"$schema" = "../../schemas/schema.json" + +# Scopes Section +# This section contains scopes, which define file level access + +[[permission]] +identifier = "scope-data-recursive" +description = "This scope permits recursive access to the complete `$DATA` folder, including sub directories and files." + +[[permission.scope.allow]] +path = "$DATA" +[[permission.scope.allow]] +path = "$DATA/**" + +[[permission]] +identifier = "scope-data" +description = "This scope permits access to all files and list content of top level directories in the `$DATA` folder." + +[[permission.scope.allow]] +path = "$DATA" +[[permission.scope.allow]] +path = "$DATA/*" + +[[permission]] +identifier = "scope-data-index" +description = "This scope permits to list all files and folders in the `$DATA`folder." + +[[permission.scope.allow]] +path = "$DATA" + +# Sets Section +# This section combines the scope elements with enablement of commands + +[[set]] +identifier = "allow-data-read-recursive" +description = "This allows full recursive read access to the complete `$DATA` folder, files and subdirectories." +permissions = [ + "read-all", + "scope-data-recursive" +] + +[[set]] +identifier = "allow-data-write-recursive" +description = "This allows full recursive write access to the complete `$DATA` folder, files and subdirectories." +permissions = [ + "write-all", + "scope-data-recursive" +] + +[[set]] +identifier = "allow-data-read" +description = "This allows non-recursive read access to the `$DATA` folder." +permissions = [ + "read-all", + "scope-data" +] + +[[set]] +identifier = "allow-data-write" +description = "This allows non-recursive write access to the `$DATA` folder." +permissions = [ + "write-all", + "scope-data" +] + +[[set]] +identifier = "allow-data-meta-recursive" +description = "This allows full recursive read access to metadata of the `$DATA` folder, including file listing and statistics." +permissions = [ + "read-meta", + "scope-data-recursive" +] + +[[set]] +identifier = "allow-data-meta" +description = "This allows non-recursive read access to metadata of the `$DATA` folder, including file listing and statistics." +permissions = [ + "read-meta", + "scope-data-index" +] \ No newline at end of file diff --git a/plugins/fs/permissions/autogenerated/base-directories/desktop.toml b/plugins/fs/permissions/autogenerated/base-directories/desktop.toml new file mode 100644 index 0000000..da369fa --- /dev/null +++ b/plugins/fs/permissions/autogenerated/base-directories/desktop.toml @@ -0,0 +1,82 @@ +# Automatically generated - DO NOT EDIT! + +"$schema" = "../../schemas/schema.json" + +# Scopes Section +# This section contains scopes, which define file level access + +[[permission]] +identifier = "scope-desktop-recursive" +description = "This scope permits recursive access to the complete `$DESKTOP` folder, including sub directories and files." + +[[permission.scope.allow]] +path = "$DESKTOP" +[[permission.scope.allow]] +path = "$DESKTOP/**" + +[[permission]] +identifier = "scope-desktop" +description = "This scope permits access to all files and list content of top level directories in the `$DESKTOP` folder." + +[[permission.scope.allow]] +path = "$DESKTOP" +[[permission.scope.allow]] +path = "$DESKTOP/*" + +[[permission]] +identifier = "scope-desktop-index" +description = "This scope permits to list all files and folders in the `$DESKTOP`folder." + +[[permission.scope.allow]] +path = "$DESKTOP" + +# Sets Section +# This section combines the scope elements with enablement of commands + +[[set]] +identifier = "allow-desktop-read-recursive" +description = "This allows full recursive read access to the complete `$DESKTOP` folder, files and subdirectories." +permissions = [ + "read-all", + "scope-desktop-recursive" +] + +[[set]] +identifier = "allow-desktop-write-recursive" +description = "This allows full recursive write access to the complete `$DESKTOP` folder, files and subdirectories." +permissions = [ + "write-all", + "scope-desktop-recursive" +] + +[[set]] +identifier = "allow-desktop-read" +description = "This allows non-recursive read access to the `$DESKTOP` folder." +permissions = [ + "read-all", + "scope-desktop" +] + +[[set]] +identifier = "allow-desktop-write" +description = "This allows non-recursive write access to the `$DESKTOP` folder." +permissions = [ + "write-all", + "scope-desktop" +] + +[[set]] +identifier = "allow-desktop-meta-recursive" +description = "This allows full recursive read access to metadata of the `$DESKTOP` folder, including file listing and statistics." +permissions = [ + "read-meta", + "scope-desktop-recursive" +] + +[[set]] +identifier = "allow-desktop-meta" +description = "This allows non-recursive read access to metadata of the `$DESKTOP` folder, including file listing and statistics." +permissions = [ + "read-meta", + "scope-desktop-index" +] \ No newline at end of file diff --git a/plugins/fs/permissions/autogenerated/base-directories/document.toml b/plugins/fs/permissions/autogenerated/base-directories/document.toml new file mode 100644 index 0000000..9feb4d0 --- /dev/null +++ b/plugins/fs/permissions/autogenerated/base-directories/document.toml @@ -0,0 +1,82 @@ +# Automatically generated - DO NOT EDIT! + +"$schema" = "../../schemas/schema.json" + +# Scopes Section +# This section contains scopes, which define file level access + +[[permission]] +identifier = "scope-document-recursive" +description = "This scope permits recursive access to the complete `$DOCUMENT` folder, including sub directories and files." + +[[permission.scope.allow]] +path = "$DOCUMENT" +[[permission.scope.allow]] +path = "$DOCUMENT/**" + +[[permission]] +identifier = "scope-document" +description = "This scope permits access to all files and list content of top level directories in the `$DOCUMENT` folder." + +[[permission.scope.allow]] +path = "$DOCUMENT" +[[permission.scope.allow]] +path = "$DOCUMENT/*" + +[[permission]] +identifier = "scope-document-index" +description = "This scope permits to list all files and folders in the `$DOCUMENT`folder." + +[[permission.scope.allow]] +path = "$DOCUMENT" + +# Sets Section +# This section combines the scope elements with enablement of commands + +[[set]] +identifier = "allow-document-read-recursive" +description = "This allows full recursive read access to the complete `$DOCUMENT` folder, files and subdirectories." +permissions = [ + "read-all", + "scope-document-recursive" +] + +[[set]] +identifier = "allow-document-write-recursive" +description = "This allows full recursive write access to the complete `$DOCUMENT` folder, files and subdirectories." +permissions = [ + "write-all", + "scope-document-recursive" +] + +[[set]] +identifier = "allow-document-read" +description = "This allows non-recursive read access to the `$DOCUMENT` folder." +permissions = [ + "read-all", + "scope-document" +] + +[[set]] +identifier = "allow-document-write" +description = "This allows non-recursive write access to the `$DOCUMENT` folder." +permissions = [ + "write-all", + "scope-document" +] + +[[set]] +identifier = "allow-document-meta-recursive" +description = "This allows full recursive read access to metadata of the `$DOCUMENT` folder, including file listing and statistics." +permissions = [ + "read-meta", + "scope-document-recursive" +] + +[[set]] +identifier = "allow-document-meta" +description = "This allows non-recursive read access to metadata of the `$DOCUMENT` folder, including file listing and statistics." +permissions = [ + "read-meta", + "scope-document-index" +] \ No newline at end of file diff --git a/plugins/fs/permissions/autogenerated/base-directories/download.toml b/plugins/fs/permissions/autogenerated/base-directories/download.toml new file mode 100644 index 0000000..8659e3a --- /dev/null +++ b/plugins/fs/permissions/autogenerated/base-directories/download.toml @@ -0,0 +1,82 @@ +# Automatically generated - DO NOT EDIT! + +"$schema" = "../../schemas/schema.json" + +# Scopes Section +# This section contains scopes, which define file level access + +[[permission]] +identifier = "scope-download-recursive" +description = "This scope permits recursive access to the complete `$DOWNLOAD` folder, including sub directories and files." + +[[permission.scope.allow]] +path = "$DOWNLOAD" +[[permission.scope.allow]] +path = "$DOWNLOAD/**" + +[[permission]] +identifier = "scope-download" +description = "This scope permits access to all files and list content of top level directories in the `$DOWNLOAD` folder." + +[[permission.scope.allow]] +path = "$DOWNLOAD" +[[permission.scope.allow]] +path = "$DOWNLOAD/*" + +[[permission]] +identifier = "scope-download-index" +description = "This scope permits to list all files and folders in the `$DOWNLOAD`folder." + +[[permission.scope.allow]] +path = "$DOWNLOAD" + +# Sets Section +# This section combines the scope elements with enablement of commands + +[[set]] +identifier = "allow-download-read-recursive" +description = "This allows full recursive read access to the complete `$DOWNLOAD` folder, files and subdirectories." +permissions = [ + "read-all", + "scope-download-recursive" +] + +[[set]] +identifier = "allow-download-write-recursive" +description = "This allows full recursive write access to the complete `$DOWNLOAD` folder, files and subdirectories." +permissions = [ + "write-all", + "scope-download-recursive" +] + +[[set]] +identifier = "allow-download-read" +description = "This allows non-recursive read access to the `$DOWNLOAD` folder." +permissions = [ + "read-all", + "scope-download" +] + +[[set]] +identifier = "allow-download-write" +description = "This allows non-recursive write access to the `$DOWNLOAD` folder." +permissions = [ + "write-all", + "scope-download" +] + +[[set]] +identifier = "allow-download-meta-recursive" +description = "This allows full recursive read access to metadata of the `$DOWNLOAD` folder, including file listing and statistics." +permissions = [ + "read-meta", + "scope-download-recursive" +] + +[[set]] +identifier = "allow-download-meta" +description = "This allows non-recursive read access to metadata of the `$DOWNLOAD` folder, including file listing and statistics." +permissions = [ + "read-meta", + "scope-download-index" +] \ No newline at end of file diff --git a/plugins/fs/permissions/autogenerated/base-directories/exe.toml b/plugins/fs/permissions/autogenerated/base-directories/exe.toml new file mode 100644 index 0000000..94950e8 --- /dev/null +++ b/plugins/fs/permissions/autogenerated/base-directories/exe.toml @@ -0,0 +1,82 @@ +# Automatically generated - DO NOT EDIT! + +"$schema" = "../../schemas/schema.json" + +# Scopes Section +# This section contains scopes, which define file level access + +[[permission]] +identifier = "scope-exe-recursive" +description = "This scope permits recursive access to the complete `$EXE` folder, including sub directories and files." + +[[permission.scope.allow]] +path = "$EXE" +[[permission.scope.allow]] +path = "$EXE/**" + +[[permission]] +identifier = "scope-exe" +description = "This scope permits access to all files and list content of top level directories in the `$EXE` folder." + +[[permission.scope.allow]] +path = "$EXE" +[[permission.scope.allow]] +path = "$EXE/*" + +[[permission]] +identifier = "scope-exe-index" +description = "This scope permits to list all files and folders in the `$EXE`folder." + +[[permission.scope.allow]] +path = "$EXE" + +# Sets Section +# This section combines the scope elements with enablement of commands + +[[set]] +identifier = "allow-exe-read-recursive" +description = "This allows full recursive read access to the complete `$EXE` folder, files and subdirectories." +permissions = [ + "read-all", + "scope-exe-recursive" +] + +[[set]] +identifier = "allow-exe-write-recursive" +description = "This allows full recursive write access to the complete `$EXE` folder, files and subdirectories." +permissions = [ + "write-all", + "scope-exe-recursive" +] + +[[set]] +identifier = "allow-exe-read" +description = "This allows non-recursive read access to the `$EXE` folder." +permissions = [ + "read-all", + "scope-exe" +] + +[[set]] +identifier = "allow-exe-write" +description = "This allows non-recursive write access to the `$EXE` folder." +permissions = [ + "write-all", + "scope-exe" +] + +[[set]] +identifier = "allow-exe-meta-recursive" +description = "This allows full recursive read access to metadata of the `$EXE` folder, including file listing and statistics." +permissions = [ + "read-meta", + "scope-exe-recursive" +] + +[[set]] +identifier = "allow-exe-meta" +description = "This allows non-recursive read access to metadata of the `$EXE` folder, including file listing and statistics." +permissions = [ + "read-meta", + "scope-exe-index" +] \ No newline at end of file diff --git a/plugins/fs/permissions/autogenerated/base-directories/font.toml b/plugins/fs/permissions/autogenerated/base-directories/font.toml new file mode 100644 index 0000000..2184004 --- /dev/null +++ b/plugins/fs/permissions/autogenerated/base-directories/font.toml @@ -0,0 +1,82 @@ +# Automatically generated - DO NOT EDIT! + +"$schema" = "../../schemas/schema.json" + +# Scopes Section +# This section contains scopes, which define file level access + +[[permission]] +identifier = "scope-font-recursive" +description = "This scope permits recursive access to the complete `$FONT` folder, including sub directories and files." + +[[permission.scope.allow]] +path = "$FONT" +[[permission.scope.allow]] +path = "$FONT/**" + +[[permission]] +identifier = "scope-font" +description = "This scope permits access to all files and list content of top level directories in the `$FONT` folder." + +[[permission.scope.allow]] +path = "$FONT" +[[permission.scope.allow]] +path = "$FONT/*" + +[[permission]] +identifier = "scope-font-index" +description = "This scope permits to list all files and folders in the `$FONT`folder." + +[[permission.scope.allow]] +path = "$FONT" + +# Sets Section +# This section combines the scope elements with enablement of commands + +[[set]] +identifier = "allow-font-read-recursive" +description = "This allows full recursive read access to the complete `$FONT` folder, files and subdirectories." +permissions = [ + "read-all", + "scope-font-recursive" +] + +[[set]] +identifier = "allow-font-write-recursive" +description = "This allows full recursive write access to the complete `$FONT` folder, files and subdirectories." +permissions = [ + "write-all", + "scope-font-recursive" +] + +[[set]] +identifier = "allow-font-read" +description = "This allows non-recursive read access to the `$FONT` folder." +permissions = [ + "read-all", + "scope-font" +] + +[[set]] +identifier = "allow-font-write" +description = "This allows non-recursive write access to the `$FONT` folder." +permissions = [ + "write-all", + "scope-font" +] + +[[set]] +identifier = "allow-font-meta-recursive" +description = "This allows full recursive read access to metadata of the `$FONT` folder, including file listing and statistics." +permissions = [ + "read-meta", + "scope-font-recursive" +] + +[[set]] +identifier = "allow-font-meta" +description = "This allows non-recursive read access to metadata of the `$FONT` folder, including file listing and statistics." +permissions = [ + "read-meta", + "scope-font-index" +] \ No newline at end of file diff --git a/plugins/fs/permissions/autogenerated/base-directories/home.toml b/plugins/fs/permissions/autogenerated/base-directories/home.toml new file mode 100644 index 0000000..cbf48e2 --- /dev/null +++ b/plugins/fs/permissions/autogenerated/base-directories/home.toml @@ -0,0 +1,82 @@ +# Automatically generated - DO NOT EDIT! + +"$schema" = "../../schemas/schema.json" + +# Scopes Section +# This section contains scopes, which define file level access + +[[permission]] +identifier = "scope-home-recursive" +description = "This scope permits recursive access to the complete `$HOME` folder, including sub directories and files." + +[[permission.scope.allow]] +path = "$HOME" +[[permission.scope.allow]] +path = "$HOME/**" + +[[permission]] +identifier = "scope-home" +description = "This scope permits access to all files and list content of top level directories in the `$HOME` folder." + +[[permission.scope.allow]] +path = "$HOME" +[[permission.scope.allow]] +path = "$HOME/*" + +[[permission]] +identifier = "scope-home-index" +description = "This scope permits to list all files and folders in the `$HOME`folder." + +[[permission.scope.allow]] +path = "$HOME" + +# Sets Section +# This section combines the scope elements with enablement of commands + +[[set]] +identifier = "allow-home-read-recursive" +description = "This allows full recursive read access to the complete `$HOME` folder, files and subdirectories." +permissions = [ + "read-all", + "scope-home-recursive" +] + +[[set]] +identifier = "allow-home-write-recursive" +description = "This allows full recursive write access to the complete `$HOME` folder, files and subdirectories." +permissions = [ + "write-all", + "scope-home-recursive" +] + +[[set]] +identifier = "allow-home-read" +description = "This allows non-recursive read access to the `$HOME` folder." +permissions = [ + "read-all", + "scope-home" +] + +[[set]] +identifier = "allow-home-write" +description = "This allows non-recursive write access to the `$HOME` folder." +permissions = [ + "write-all", + "scope-home" +] + +[[set]] +identifier = "allow-home-meta-recursive" +description = "This allows full recursive read access to metadata of the `$HOME` folder, including file listing and statistics." +permissions = [ + "read-meta", + "scope-home-recursive" +] + +[[set]] +identifier = "allow-home-meta" +description = "This allows non-recursive read access to metadata of the `$HOME` folder, including file listing and statistics." +permissions = [ + "read-meta", + "scope-home-index" +] \ No newline at end of file diff --git a/plugins/fs/permissions/autogenerated/base-directories/localdata.toml b/plugins/fs/permissions/autogenerated/base-directories/localdata.toml new file mode 100644 index 0000000..90a8f48 --- /dev/null +++ b/plugins/fs/permissions/autogenerated/base-directories/localdata.toml @@ -0,0 +1,82 @@ +# Automatically generated - DO NOT EDIT! + +"$schema" = "../../schemas/schema.json" + +# Scopes Section +# This section contains scopes, which define file level access + +[[permission]] +identifier = "scope-localdata-recursive" +description = "This scope permits recursive access to the complete `$LOCALDATA` folder, including sub directories and files." + +[[permission.scope.allow]] +path = "$LOCALDATA" +[[permission.scope.allow]] +path = "$LOCALDATA/**" + +[[permission]] +identifier = "scope-localdata" +description = "This scope permits access to all files and list content of top level directories in the `$LOCALDATA` folder." + +[[permission.scope.allow]] +path = "$LOCALDATA" +[[permission.scope.allow]] +path = "$LOCALDATA/*" + +[[permission]] +identifier = "scope-localdata-index" +description = "This scope permits to list all files and folders in the `$LOCALDATA`folder." + +[[permission.scope.allow]] +path = "$LOCALDATA" + +# Sets Section +# This section combines the scope elements with enablement of commands + +[[set]] +identifier = "allow-localdata-read-recursive" +description = "This allows full recursive read access to the complete `$LOCALDATA` folder, files and subdirectories." +permissions = [ + "read-all", + "scope-localdata-recursive" +] + +[[set]] +identifier = "allow-localdata-write-recursive" +description = "This allows full recursive write access to the complete `$LOCALDATA` folder, files and subdirectories." +permissions = [ + "write-all", + "scope-localdata-recursive" +] + +[[set]] +identifier = "allow-localdata-read" +description = "This allows non-recursive read access to the `$LOCALDATA` folder." +permissions = [ + "read-all", + "scope-localdata" +] + +[[set]] +identifier = "allow-localdata-write" +description = "This allows non-recursive write access to the `$LOCALDATA` folder." +permissions = [ + "write-all", + "scope-localdata" +] + +[[set]] +identifier = "allow-localdata-meta-recursive" +description = "This allows full recursive read access to metadata of the `$LOCALDATA` folder, including file listing and statistics." +permissions = [ + "read-meta", + "scope-localdata-recursive" +] + +[[set]] +identifier = "allow-localdata-meta" +description = "This allows non-recursive read access to metadata of the `$LOCALDATA` folder, including file listing and statistics." +permissions = [ + "read-meta", + "scope-localdata-index" +] \ No newline at end of file diff --git a/plugins/fs/permissions/autogenerated/base-directories/log.toml b/plugins/fs/permissions/autogenerated/base-directories/log.toml new file mode 100644 index 0000000..d505a3c --- /dev/null +++ b/plugins/fs/permissions/autogenerated/base-directories/log.toml @@ -0,0 +1,82 @@ +# Automatically generated - DO NOT EDIT! + +"$schema" = "../../schemas/schema.json" + +# Scopes Section +# This section contains scopes, which define file level access + +[[permission]] +identifier = "scope-log-recursive" +description = "This scope permits recursive access to the complete `$LOG` folder, including sub directories and files." + +[[permission.scope.allow]] +path = "$LOG" +[[permission.scope.allow]] +path = "$LOG/**" + +[[permission]] +identifier = "scope-log" +description = "This scope permits access to all files and list content of top level directories in the `$LOG` folder." + +[[permission.scope.allow]] +path = "$LOG" +[[permission.scope.allow]] +path = "$LOG/*" + +[[permission]] +identifier = "scope-log-index" +description = "This scope permits to list all files and folders in the `$LOG`folder." + +[[permission.scope.allow]] +path = "$LOG" + +# Sets Section +# This section combines the scope elements with enablement of commands + +[[set]] +identifier = "allow-log-read-recursive" +description = "This allows full recursive read access to the complete `$LOG` folder, files and subdirectories." +permissions = [ + "read-all", + "scope-log-recursive" +] + +[[set]] +identifier = "allow-log-write-recursive" +description = "This allows full recursive write access to the complete `$LOG` folder, files and subdirectories." +permissions = [ + "write-all", + "scope-log-recursive" +] + +[[set]] +identifier = "allow-log-read" +description = "This allows non-recursive read access to the `$LOG` folder." +permissions = [ + "read-all", + "scope-log" +] + +[[set]] +identifier = "allow-log-write" +description = "This allows non-recursive write access to the `$LOG` folder." +permissions = [ + "write-all", + "scope-log" +] + +[[set]] +identifier = "allow-log-meta-recursive" +description = "This allows full recursive read access to metadata of the `$LOG` folder, including file listing and statistics." +permissions = [ + "read-meta", + "scope-log-recursive" +] + +[[set]] +identifier = "allow-log-meta" +description = "This allows non-recursive read access to metadata of the `$LOG` folder, including file listing and statistics." +permissions = [ + "read-meta", + "scope-log-index" +] \ No newline at end of file diff --git a/plugins/fs/permissions/autogenerated/base-directories/picture.toml b/plugins/fs/permissions/autogenerated/base-directories/picture.toml new file mode 100644 index 0000000..6a76090 --- /dev/null +++ b/plugins/fs/permissions/autogenerated/base-directories/picture.toml @@ -0,0 +1,82 @@ +# Automatically generated - DO NOT EDIT! + +"$schema" = "../../schemas/schema.json" + +# Scopes Section +# This section contains scopes, which define file level access + +[[permission]] +identifier = "scope-picture-recursive" +description = "This scope permits recursive access to the complete `$PICTURE` folder, including sub directories and files." + +[[permission.scope.allow]] +path = "$PICTURE" +[[permission.scope.allow]] +path = "$PICTURE/**" + +[[permission]] +identifier = "scope-picture" +description = "This scope permits access to all files and list content of top level directories in the `$PICTURE` folder." + +[[permission.scope.allow]] +path = "$PICTURE" +[[permission.scope.allow]] +path = "$PICTURE/*" + +[[permission]] +identifier = "scope-picture-index" +description = "This scope permits to list all files and folders in the `$PICTURE`folder." + +[[permission.scope.allow]] +path = "$PICTURE" + +# Sets Section +# This section combines the scope elements with enablement of commands + +[[set]] +identifier = "allow-picture-read-recursive" +description = "This allows full recursive read access to the complete `$PICTURE` folder, files and subdirectories." +permissions = [ + "read-all", + "scope-picture-recursive" +] + +[[set]] +identifier = "allow-picture-write-recursive" +description = "This allows full recursive write access to the complete `$PICTURE` folder, files and subdirectories." +permissions = [ + "write-all", + "scope-picture-recursive" +] + +[[set]] +identifier = "allow-picture-read" +description = "This allows non-recursive read access to the `$PICTURE` folder." +permissions = [ + "read-all", + "scope-picture" +] + +[[set]] +identifier = "allow-picture-write" +description = "This allows non-recursive write access to the `$PICTURE` folder." +permissions = [ + "write-all", + "scope-picture" +] + +[[set]] +identifier = "allow-picture-meta-recursive" +description = "This allows full recursive read access to metadata of the `$PICTURE` folder, including file listing and statistics." +permissions = [ + "read-meta", + "scope-picture-recursive" +] + +[[set]] +identifier = "allow-picture-meta" +description = "This allows non-recursive read access to metadata of the `$PICTURE` folder, including file listing and statistics." +permissions = [ + "read-meta", + "scope-picture-index" +] \ No newline at end of file diff --git a/plugins/fs/permissions/autogenerated/base-directories/public.toml b/plugins/fs/permissions/autogenerated/base-directories/public.toml new file mode 100644 index 0000000..2e39abb --- /dev/null +++ b/plugins/fs/permissions/autogenerated/base-directories/public.toml @@ -0,0 +1,82 @@ +# Automatically generated - DO NOT EDIT! + +"$schema" = "../../schemas/schema.json" + +# Scopes Section +# This section contains scopes, which define file level access + +[[permission]] +identifier = "scope-public-recursive" +description = "This scope permits recursive access to the complete `$PUBLIC` folder, including sub directories and files." + +[[permission.scope.allow]] +path = "$PUBLIC" +[[permission.scope.allow]] +path = "$PUBLIC/**" + +[[permission]] +identifier = "scope-public" +description = "This scope permits access to all files and list content of top level directories in the `$PUBLIC` folder." + +[[permission.scope.allow]] +path = "$PUBLIC" +[[permission.scope.allow]] +path = "$PUBLIC/*" + +[[permission]] +identifier = "scope-public-index" +description = "This scope permits to list all files and folders in the `$PUBLIC`folder." + +[[permission.scope.allow]] +path = "$PUBLIC" + +# Sets Section +# This section combines the scope elements with enablement of commands + +[[set]] +identifier = "allow-public-read-recursive" +description = "This allows full recursive read access to the complete `$PUBLIC` folder, files and subdirectories." +permissions = [ + "read-all", + "scope-public-recursive" +] + +[[set]] +identifier = "allow-public-write-recursive" +description = "This allows full recursive write access to the complete `$PUBLIC` folder, files and subdirectories." +permissions = [ + "write-all", + "scope-public-recursive" +] + +[[set]] +identifier = "allow-public-read" +description = "This allows non-recursive read access to the `$PUBLIC` folder." +permissions = [ + "read-all", + "scope-public" +] + +[[set]] +identifier = "allow-public-write" +description = "This allows non-recursive write access to the `$PUBLIC` folder." +permissions = [ + "write-all", + "scope-public" +] + +[[set]] +identifier = "allow-public-meta-recursive" +description = "This allows full recursive read access to metadata of the `$PUBLIC` folder, including file listing and statistics." +permissions = [ + "read-meta", + "scope-public-recursive" +] + +[[set]] +identifier = "allow-public-meta" +description = "This allows non-recursive read access to metadata of the `$PUBLIC` folder, including file listing and statistics." +permissions = [ + "read-meta", + "scope-public-index" +] \ No newline at end of file diff --git a/plugins/fs/permissions/autogenerated/base-directories/resource.toml b/plugins/fs/permissions/autogenerated/base-directories/resource.toml new file mode 100644 index 0000000..53dfeb0 --- /dev/null +++ b/plugins/fs/permissions/autogenerated/base-directories/resource.toml @@ -0,0 +1,82 @@ +# Automatically generated - DO NOT EDIT! + +"$schema" = "../../schemas/schema.json" + +# Scopes Section +# This section contains scopes, which define file level access + +[[permission]] +identifier = "scope-resource-recursive" +description = "This scope permits recursive access to the complete `$RESOURCE` folder, including sub directories and files." + +[[permission.scope.allow]] +path = "$RESOURCE" +[[permission.scope.allow]] +path = "$RESOURCE/**" + +[[permission]] +identifier = "scope-resource" +description = "This scope permits access to all files and list content of top level directories in the `$RESOURCE` folder." + +[[permission.scope.allow]] +path = "$RESOURCE" +[[permission.scope.allow]] +path = "$RESOURCE/*" + +[[permission]] +identifier = "scope-resource-index" +description = "This scope permits to list all files and folders in the `$RESOURCE`folder." + +[[permission.scope.allow]] +path = "$RESOURCE" + +# Sets Section +# This section combines the scope elements with enablement of commands + +[[set]] +identifier = "allow-resource-read-recursive" +description = "This allows full recursive read access to the complete `$RESOURCE` folder, files and subdirectories." +permissions = [ + "read-all", + "scope-resource-recursive" +] + +[[set]] +identifier = "allow-resource-write-recursive" +description = "This allows full recursive write access to the complete `$RESOURCE` folder, files and subdirectories." +permissions = [ + "write-all", + "scope-resource-recursive" +] + +[[set]] +identifier = "allow-resource-read" +description = "This allows non-recursive read access to the `$RESOURCE` folder." +permissions = [ + "read-all", + "scope-resource" +] + +[[set]] +identifier = "allow-resource-write" +description = "This allows non-recursive write access to the `$RESOURCE` folder." +permissions = [ + "write-all", + "scope-resource" +] + +[[set]] +identifier = "allow-resource-meta-recursive" +description = "This allows full recursive read access to metadata of the `$RESOURCE` folder, including file listing and statistics." +permissions = [ + "read-meta", + "scope-resource-recursive" +] + +[[set]] +identifier = "allow-resource-meta" +description = "This allows non-recursive read access to metadata of the `$RESOURCE` folder, including file listing and statistics." +permissions = [ + "read-meta", + "scope-resource-index" +] \ No newline at end of file diff --git a/plugins/fs/permissions/autogenerated/base-directories/runtime.toml b/plugins/fs/permissions/autogenerated/base-directories/runtime.toml new file mode 100644 index 0000000..8dcc2a0 --- /dev/null +++ b/plugins/fs/permissions/autogenerated/base-directories/runtime.toml @@ -0,0 +1,82 @@ +# Automatically generated - DO NOT EDIT! + +"$schema" = "../../schemas/schema.json" + +# Scopes Section +# This section contains scopes, which define file level access + +[[permission]] +identifier = "scope-runtime-recursive" +description = "This scope permits recursive access to the complete `$RUNTIME` folder, including sub directories and files." + +[[permission.scope.allow]] +path = "$RUNTIME" +[[permission.scope.allow]] +path = "$RUNTIME/**" + +[[permission]] +identifier = "scope-runtime" +description = "This scope permits access to all files and list content of top level directories in the `$RUNTIME` folder." + +[[permission.scope.allow]] +path = "$RUNTIME" +[[permission.scope.allow]] +path = "$RUNTIME/*" + +[[permission]] +identifier = "scope-runtime-index" +description = "This scope permits to list all files and folders in the `$RUNTIME`folder." + +[[permission.scope.allow]] +path = "$RUNTIME" + +# Sets Section +# This section combines the scope elements with enablement of commands + +[[set]] +identifier = "allow-runtime-read-recursive" +description = "This allows full recursive read access to the complete `$RUNTIME` folder, files and subdirectories." +permissions = [ + "read-all", + "scope-runtime-recursive" +] + +[[set]] +identifier = "allow-runtime-write-recursive" +description = "This allows full recursive write access to the complete `$RUNTIME` folder, files and subdirectories." +permissions = [ + "write-all", + "scope-runtime-recursive" +] + +[[set]] +identifier = "allow-runtime-read" +description = "This allows non-recursive read access to the `$RUNTIME` folder." +permissions = [ + "read-all", + "scope-runtime" +] + +[[set]] +identifier = "allow-runtime-write" +description = "This allows non-recursive write access to the `$RUNTIME` folder." +permissions = [ + "write-all", + "scope-runtime" +] + +[[set]] +identifier = "allow-runtime-meta-recursive" +description = "This allows full recursive read access to metadata of the `$RUNTIME` folder, including file listing and statistics." +permissions = [ + "read-meta", + "scope-runtime-recursive" +] + +[[set]] +identifier = "allow-runtime-meta" +description = "This allows non-recursive read access to metadata of the `$RUNTIME` folder, including file listing and statistics." +permissions = [ + "read-meta", + "scope-runtime-index" +] \ No newline at end of file diff --git a/plugins/fs/permissions/autogenerated/base-directories/temp.toml b/plugins/fs/permissions/autogenerated/base-directories/temp.toml new file mode 100644 index 0000000..c08e1da --- /dev/null +++ b/plugins/fs/permissions/autogenerated/base-directories/temp.toml @@ -0,0 +1,82 @@ +# Automatically generated - DO NOT EDIT! + +"$schema" = "../../schemas/schema.json" + +# Scopes Section +# This section contains scopes, which define file level access + +[[permission]] +identifier = "scope-temp-recursive" +description = "This scope permits recursive access to the complete `$TEMP` folder, including sub directories and files." + +[[permission.scope.allow]] +path = "$TEMP" +[[permission.scope.allow]] +path = "$TEMP/**" + +[[permission]] +identifier = "scope-temp" +description = "This scope permits access to all files and list content of top level directories in the `$TEMP` folder." + +[[permission.scope.allow]] +path = "$TEMP" +[[permission.scope.allow]] +path = "$TEMP/*" + +[[permission]] +identifier = "scope-temp-index" +description = "This scope permits to list all files and folders in the `$TEMP`folder." + +[[permission.scope.allow]] +path = "$TEMP" + +# Sets Section +# This section combines the scope elements with enablement of commands + +[[set]] +identifier = "allow-temp-read-recursive" +description = "This allows full recursive read access to the complete `$TEMP` folder, files and subdirectories." +permissions = [ + "read-all", + "scope-temp-recursive" +] + +[[set]] +identifier = "allow-temp-write-recursive" +description = "This allows full recursive write access to the complete `$TEMP` folder, files and subdirectories." +permissions = [ + "write-all", + "scope-temp-recursive" +] + +[[set]] +identifier = "allow-temp-read" +description = "This allows non-recursive read access to the `$TEMP` folder." +permissions = [ + "read-all", + "scope-temp" +] + +[[set]] +identifier = "allow-temp-write" +description = "This allows non-recursive write access to the `$TEMP` folder." +permissions = [ + "write-all", + "scope-temp" +] + +[[set]] +identifier = "allow-temp-meta-recursive" +description = "This allows full recursive read access to metadata of the `$TEMP` folder, including file listing and statistics." +permissions = [ + "read-meta", + "scope-temp-recursive" +] + +[[set]] +identifier = "allow-temp-meta" +description = "This allows non-recursive read access to metadata of the `$TEMP` folder, including file listing and statistics." +permissions = [ + "read-meta", + "scope-temp-index" +] \ No newline at end of file diff --git a/plugins/fs/permissions/autogenerated/base-directories/template.toml b/plugins/fs/permissions/autogenerated/base-directories/template.toml new file mode 100644 index 0000000..ce39f77 --- /dev/null +++ b/plugins/fs/permissions/autogenerated/base-directories/template.toml @@ -0,0 +1,82 @@ +# Automatically generated - DO NOT EDIT! + +"$schema" = "../../schemas/schema.json" + +# Scopes Section +# This section contains scopes, which define file level access + +[[permission]] +identifier = "scope-template-recursive" +description = "This scope permits recursive access to the complete `$TEMPLATE` folder, including sub directories and files." + +[[permission.scope.allow]] +path = "$TEMPLATE" +[[permission.scope.allow]] +path = "$TEMPLATE/**" + +[[permission]] +identifier = "scope-template" +description = "This scope permits access to all files and list content of top level directories in the `$TEMPLATE` folder." + +[[permission.scope.allow]] +path = "$TEMPLATE" +[[permission.scope.allow]] +path = "$TEMPLATE/*" + +[[permission]] +identifier = "scope-template-index" +description = "This scope permits to list all files and folders in the `$TEMPLATE`folder." + +[[permission.scope.allow]] +path = "$TEMPLATE" + +# Sets Section +# This section combines the scope elements with enablement of commands + +[[set]] +identifier = "allow-template-read-recursive" +description = "This allows full recursive read access to the complete `$TEMPLATE` folder, files and subdirectories." +permissions = [ + "read-all", + "scope-template-recursive" +] + +[[set]] +identifier = "allow-template-write-recursive" +description = "This allows full recursive write access to the complete `$TEMPLATE` folder, files and subdirectories." +permissions = [ + "write-all", + "scope-template-recursive" +] + +[[set]] +identifier = "allow-template-read" +description = "This allows non-recursive read access to the `$TEMPLATE` folder." +permissions = [ + "read-all", + "scope-template" +] + +[[set]] +identifier = "allow-template-write" +description = "This allows non-recursive write access to the `$TEMPLATE` folder." +permissions = [ + "write-all", + "scope-template" +] + +[[set]] +identifier = "allow-template-meta-recursive" +description = "This allows full recursive read access to metadata of the `$TEMPLATE` folder, including file listing and statistics." +permissions = [ + "read-meta", + "scope-template-recursive" +] + +[[set]] +identifier = "allow-template-meta" +description = "This allows non-recursive read access to metadata of the `$TEMPLATE` folder, including file listing and statistics." +permissions = [ + "read-meta", + "scope-template-index" +] \ No newline at end of file diff --git a/plugins/fs/permissions/autogenerated/base-directories/video.toml b/plugins/fs/permissions/autogenerated/base-directories/video.toml new file mode 100644 index 0000000..df41abd --- /dev/null +++ b/plugins/fs/permissions/autogenerated/base-directories/video.toml @@ -0,0 +1,82 @@ +# Automatically generated - DO NOT EDIT! + +"$schema" = "../../schemas/schema.json" + +# Scopes Section +# This section contains scopes, which define file level access + +[[permission]] +identifier = "scope-video-recursive" +description = "This scope permits recursive access to the complete `$VIDEO` folder, including sub directories and files." + +[[permission.scope.allow]] +path = "$VIDEO" +[[permission.scope.allow]] +path = "$VIDEO/**" + +[[permission]] +identifier = "scope-video" +description = "This scope permits access to all files and list content of top level directories in the `$VIDEO` folder." + +[[permission.scope.allow]] +path = "$VIDEO" +[[permission.scope.allow]] +path = "$VIDEO/*" + +[[permission]] +identifier = "scope-video-index" +description = "This scope permits to list all files and folders in the `$VIDEO`folder." + +[[permission.scope.allow]] +path = "$VIDEO" + +# Sets Section +# This section combines the scope elements with enablement of commands + +[[set]] +identifier = "allow-video-read-recursive" +description = "This allows full recursive read access to the complete `$VIDEO` folder, files and subdirectories." +permissions = [ + "read-all", + "scope-video-recursive" +] + +[[set]] +identifier = "allow-video-write-recursive" +description = "This allows full recursive write access to the complete `$VIDEO` folder, files and subdirectories." +permissions = [ + "write-all", + "scope-video-recursive" +] + +[[set]] +identifier = "allow-video-read" +description = "This allows non-recursive read access to the `$VIDEO` folder." +permissions = [ + "read-all", + "scope-video" +] + +[[set]] +identifier = "allow-video-write" +description = "This allows non-recursive write access to the `$VIDEO` folder." +permissions = [ + "write-all", + "scope-video" +] + +[[set]] +identifier = "allow-video-meta-recursive" +description = "This allows full recursive read access to metadata of the `$VIDEO` folder, including file listing and statistics." +permissions = [ + "read-meta", + "scope-video-recursive" +] + +[[set]] +identifier = "allow-video-meta" +description = "This allows non-recursive read access to metadata of the `$VIDEO` folder, including file listing and statistics." +permissions = [ + "read-meta", + "scope-video-index" +] \ No newline at end of file diff --git a/plugins/fs/permissions/autogenerated/commands/copy_file.toml b/plugins/fs/permissions/autogenerated/commands/copy_file.toml new file mode 100644 index 0000000..61bedf9 --- /dev/null +++ b/plugins/fs/permissions/autogenerated/commands/copy_file.toml @@ -0,0 +1,13 @@ +# Automatically generated - DO NOT EDIT! + +"$schema" = "../../schemas/schema.json" + +[[permission]] +identifier = "allow-copy-file" +description = "Enables the copy_file command without any pre-configured scope." +commands.allow = ["copy_file"] + +[[permission]] +identifier = "deny-copy-file" +description = "Denies the copy_file command without any pre-configured scope." +commands.deny = ["copy_file"] diff --git a/plugins/fs/permissions/autogenerated/commands/create.toml b/plugins/fs/permissions/autogenerated/commands/create.toml new file mode 100644 index 0000000..6646cc6 --- /dev/null +++ b/plugins/fs/permissions/autogenerated/commands/create.toml @@ -0,0 +1,13 @@ +# Automatically generated - DO NOT EDIT! + +"$schema" = "../../schemas/schema.json" + +[[permission]] +identifier = "allow-create" +description = "Enables the create command without any pre-configured scope." +commands.allow = ["create"] + +[[permission]] +identifier = "deny-create" +description = "Denies the create command without any pre-configured scope." +commands.deny = ["create"] diff --git a/plugins/fs/permissions/autogenerated/commands/exists.toml b/plugins/fs/permissions/autogenerated/commands/exists.toml new file mode 100644 index 0000000..0eed148 --- /dev/null +++ b/plugins/fs/permissions/autogenerated/commands/exists.toml @@ -0,0 +1,13 @@ +# Automatically generated - DO NOT EDIT! + +"$schema" = "../../schemas/schema.json" + +[[permission]] +identifier = "allow-exists" +description = "Enables the exists command without any pre-configured scope." +commands.allow = ["exists"] + +[[permission]] +identifier = "deny-exists" +description = "Denies the exists command without any pre-configured scope." +commands.deny = ["exists"] diff --git a/plugins/fs/permissions/autogenerated/commands/fstat.toml b/plugins/fs/permissions/autogenerated/commands/fstat.toml new file mode 100644 index 0000000..30d2811 --- /dev/null +++ b/plugins/fs/permissions/autogenerated/commands/fstat.toml @@ -0,0 +1,13 @@ +# Automatically generated - DO NOT EDIT! + +"$schema" = "../../schemas/schema.json" + +[[permission]] +identifier = "allow-fstat" +description = "Enables the fstat command without any pre-configured scope." +commands.allow = ["fstat"] + +[[permission]] +identifier = "deny-fstat" +description = "Denies the fstat command without any pre-configured scope." +commands.deny = ["fstat"] diff --git a/plugins/fs/permissions/autogenerated/commands/ftruncate.toml b/plugins/fs/permissions/autogenerated/commands/ftruncate.toml new file mode 100644 index 0000000..6b54ffe --- /dev/null +++ b/plugins/fs/permissions/autogenerated/commands/ftruncate.toml @@ -0,0 +1,13 @@ +# Automatically generated - DO NOT EDIT! + +"$schema" = "../../schemas/schema.json" + +[[permission]] +identifier = "allow-ftruncate" +description = "Enables the ftruncate command without any pre-configured scope." +commands.allow = ["ftruncate"] + +[[permission]] +identifier = "deny-ftruncate" +description = "Denies the ftruncate command without any pre-configured scope." +commands.deny = ["ftruncate"] diff --git a/plugins/fs/permissions/autogenerated/commands/lstat.toml b/plugins/fs/permissions/autogenerated/commands/lstat.toml new file mode 100644 index 0000000..b224635 --- /dev/null +++ b/plugins/fs/permissions/autogenerated/commands/lstat.toml @@ -0,0 +1,13 @@ +# Automatically generated - DO NOT EDIT! + +"$schema" = "../../schemas/schema.json" + +[[permission]] +identifier = "allow-lstat" +description = "Enables the lstat command without any pre-configured scope." +commands.allow = ["lstat"] + +[[permission]] +identifier = "deny-lstat" +description = "Denies the lstat command without any pre-configured scope." +commands.deny = ["lstat"] diff --git a/plugins/fs/permissions/autogenerated/commands/mkdir.toml b/plugins/fs/permissions/autogenerated/commands/mkdir.toml new file mode 100644 index 0000000..58cdbbc --- /dev/null +++ b/plugins/fs/permissions/autogenerated/commands/mkdir.toml @@ -0,0 +1,13 @@ +# Automatically generated - DO NOT EDIT! + +"$schema" = "../../schemas/schema.json" + +[[permission]] +identifier = "allow-mkdir" +description = "Enables the mkdir command without any pre-configured scope." +commands.allow = ["mkdir"] + +[[permission]] +identifier = "deny-mkdir" +description = "Denies the mkdir command without any pre-configured scope." +commands.deny = ["mkdir"] diff --git a/plugins/fs/permissions/autogenerated/commands/open.toml b/plugins/fs/permissions/autogenerated/commands/open.toml new file mode 100644 index 0000000..4ea6dff --- /dev/null +++ b/plugins/fs/permissions/autogenerated/commands/open.toml @@ -0,0 +1,13 @@ +# Automatically generated - DO NOT EDIT! + +"$schema" = "../../schemas/schema.json" + +[[permission]] +identifier = "allow-open" +description = "Enables the open command without any pre-configured scope." +commands.allow = ["open"] + +[[permission]] +identifier = "deny-open" +description = "Denies the open command without any pre-configured scope." +commands.deny = ["open"] diff --git a/plugins/fs/permissions/autogenerated/commands/read.toml b/plugins/fs/permissions/autogenerated/commands/read.toml new file mode 100644 index 0000000..20fa10c --- /dev/null +++ b/plugins/fs/permissions/autogenerated/commands/read.toml @@ -0,0 +1,13 @@ +# Automatically generated - DO NOT EDIT! + +"$schema" = "../../schemas/schema.json" + +[[permission]] +identifier = "allow-read" +description = "Enables the read command without any pre-configured scope." +commands.allow = ["read"] + +[[permission]] +identifier = "deny-read" +description = "Denies the read command without any pre-configured scope." +commands.deny = ["read"] diff --git a/plugins/fs/permissions/autogenerated/commands/read_dir.toml b/plugins/fs/permissions/autogenerated/commands/read_dir.toml new file mode 100644 index 0000000..eef68eb --- /dev/null +++ b/plugins/fs/permissions/autogenerated/commands/read_dir.toml @@ -0,0 +1,13 @@ +# Automatically generated - DO NOT EDIT! + +"$schema" = "../../schemas/schema.json" + +[[permission]] +identifier = "allow-read-dir" +description = "Enables the read_dir command without any pre-configured scope." +commands.allow = ["read_dir"] + +[[permission]] +identifier = "deny-read-dir" +description = "Denies the read_dir command without any pre-configured scope." +commands.deny = ["read_dir"] diff --git a/plugins/fs/permissions/autogenerated/commands/read_file.toml b/plugins/fs/permissions/autogenerated/commands/read_file.toml new file mode 100644 index 0000000..b932b71 --- /dev/null +++ b/plugins/fs/permissions/autogenerated/commands/read_file.toml @@ -0,0 +1,13 @@ +# Automatically generated - DO NOT EDIT! + +"$schema" = "../../schemas/schema.json" + +[[permission]] +identifier = "allow-read-file" +description = "Enables the read_file command without any pre-configured scope." +commands.allow = ["read_file"] + +[[permission]] +identifier = "deny-read-file" +description = "Denies the read_file command without any pre-configured scope." +commands.deny = ["read_file"] diff --git a/plugins/fs/permissions/autogenerated/commands/read_text_file.toml b/plugins/fs/permissions/autogenerated/commands/read_text_file.toml new file mode 100644 index 0000000..7a25115 --- /dev/null +++ b/plugins/fs/permissions/autogenerated/commands/read_text_file.toml @@ -0,0 +1,13 @@ +# Automatically generated - DO NOT EDIT! + +"$schema" = "../../schemas/schema.json" + +[[permission]] +identifier = "allow-read-text-file" +description = "Enables the read_text_file command without any pre-configured scope." +commands.allow = ["read_text_file"] + +[[permission]] +identifier = "deny-read-text-file" +description = "Denies the read_text_file command without any pre-configured scope." +commands.deny = ["read_text_file"] diff --git a/plugins/fs/permissions/autogenerated/commands/read_text_file_lines.toml b/plugins/fs/permissions/autogenerated/commands/read_text_file_lines.toml new file mode 100644 index 0000000..84b4ebb --- /dev/null +++ b/plugins/fs/permissions/autogenerated/commands/read_text_file_lines.toml @@ -0,0 +1,22 @@ +# Automatically generated - DO NOT EDIT! + +"$schema" = "../../schemas/schema.json" + +[[permission]] +identifier = "allow-read-text-file-lines" +description = "Enables the read_text_file_lines command without any pre-configured scope." + +[permission.commands] +allow = [ + "read_text_file_lines", + "read_text_file_lines_next", +] +deny = [] + +[[permission]] +identifier = "deny-read-text-file-lines" +description = "Denies the read_text_file_lines command without any pre-configured scope." + +[permission.commands] +allow = [] +deny = ["read_text_file_lines"] diff --git a/plugins/fs/permissions/autogenerated/commands/read_text_file_lines_next.toml b/plugins/fs/permissions/autogenerated/commands/read_text_file_lines_next.toml new file mode 100644 index 0000000..021ea37 --- /dev/null +++ b/plugins/fs/permissions/autogenerated/commands/read_text_file_lines_next.toml @@ -0,0 +1,13 @@ +# Automatically generated - DO NOT EDIT! + +"$schema" = "../../schemas/schema.json" + +[[permission]] +identifier = "allow-read-text-file-lines-next" +description = "Enables the read_text_file_lines_next command without any pre-configured scope." +commands.allow = ["read_text_file_lines_next"] + +[[permission]] +identifier = "deny-read-text-file-lines-next" +description = "Denies the read_text_file_lines_next command without any pre-configured scope." +commands.deny = ["read_text_file_lines_next"] diff --git a/plugins/fs/permissions/autogenerated/commands/remove.toml b/plugins/fs/permissions/autogenerated/commands/remove.toml new file mode 100644 index 0000000..9c9791e --- /dev/null +++ b/plugins/fs/permissions/autogenerated/commands/remove.toml @@ -0,0 +1,13 @@ +# Automatically generated - DO NOT EDIT! + +"$schema" = "../../schemas/schema.json" + +[[permission]] +identifier = "allow-remove" +description = "Enables the remove command without any pre-configured scope." +commands.allow = ["remove"] + +[[permission]] +identifier = "deny-remove" +description = "Denies the remove command without any pre-configured scope." +commands.deny = ["remove"] diff --git a/plugins/fs/permissions/autogenerated/commands/rename.toml b/plugins/fs/permissions/autogenerated/commands/rename.toml new file mode 100644 index 0000000..91def18 --- /dev/null +++ b/plugins/fs/permissions/autogenerated/commands/rename.toml @@ -0,0 +1,13 @@ +# Automatically generated - DO NOT EDIT! + +"$schema" = "../../schemas/schema.json" + +[[permission]] +identifier = "allow-rename" +description = "Enables the rename command without any pre-configured scope." +commands.allow = ["rename"] + +[[permission]] +identifier = "deny-rename" +description = "Denies the rename command without any pre-configured scope." +commands.deny = ["rename"] diff --git a/plugins/fs/permissions/autogenerated/commands/seek.toml b/plugins/fs/permissions/autogenerated/commands/seek.toml new file mode 100644 index 0000000..cb21bdc --- /dev/null +++ b/plugins/fs/permissions/autogenerated/commands/seek.toml @@ -0,0 +1,13 @@ +# Automatically generated - DO NOT EDIT! + +"$schema" = "../../schemas/schema.json" + +[[permission]] +identifier = "allow-seek" +description = "Enables the seek command without any pre-configured scope." +commands.allow = ["seek"] + +[[permission]] +identifier = "deny-seek" +description = "Denies the seek command without any pre-configured scope." +commands.deny = ["seek"] diff --git a/plugins/fs/permissions/autogenerated/commands/size.toml b/plugins/fs/permissions/autogenerated/commands/size.toml new file mode 100644 index 0000000..8a0ea55 --- /dev/null +++ b/plugins/fs/permissions/autogenerated/commands/size.toml @@ -0,0 +1,13 @@ +# Automatically generated - DO NOT EDIT! + +"$schema" = "../../schemas/schema.json" + +[[permission]] +identifier = "allow-size" +description = "Enables the size command without any pre-configured scope." +commands.allow = ["size"] + +[[permission]] +identifier = "deny-size" +description = "Denies the size command without any pre-configured scope." +commands.deny = ["size"] diff --git a/plugins/fs/permissions/autogenerated/commands/start_accessing_security_scoped_resource.toml b/plugins/fs/permissions/autogenerated/commands/start_accessing_security_scoped_resource.toml new file mode 100644 index 0000000..d14c502 --- /dev/null +++ b/plugins/fs/permissions/autogenerated/commands/start_accessing_security_scoped_resource.toml @@ -0,0 +1,13 @@ +# Automatically generated - DO NOT EDIT! + +"$schema" = "../../schemas/schema.json" + +[[permission]] +identifier = "allow-start-accessing-security-scoped-resource" +description = "Enables the start_accessing_security_scoped_resource command without any pre-configured scope." +commands.allow = ["start_accessing_security_scoped_resource"] + +[[permission]] +identifier = "deny-start-accessing-security-scoped-resource" +description = "Denies the start_accessing_security_scoped_resource command without any pre-configured scope." +commands.deny = ["start_accessing_security_scoped_resource"] diff --git a/plugins/fs/permissions/autogenerated/commands/stat.toml b/plugins/fs/permissions/autogenerated/commands/stat.toml new file mode 100644 index 0000000..56f751b --- /dev/null +++ b/plugins/fs/permissions/autogenerated/commands/stat.toml @@ -0,0 +1,13 @@ +# Automatically generated - DO NOT EDIT! + +"$schema" = "../../schemas/schema.json" + +[[permission]] +identifier = "allow-stat" +description = "Enables the stat command without any pre-configured scope." +commands.allow = ["stat"] + +[[permission]] +identifier = "deny-stat" +description = "Denies the stat command without any pre-configured scope." +commands.deny = ["stat"] diff --git a/plugins/fs/permissions/autogenerated/commands/stop_accessing_security_scoped_resource.toml b/plugins/fs/permissions/autogenerated/commands/stop_accessing_security_scoped_resource.toml new file mode 100644 index 0000000..fae3f19 --- /dev/null +++ b/plugins/fs/permissions/autogenerated/commands/stop_accessing_security_scoped_resource.toml @@ -0,0 +1,13 @@ +# Automatically generated - DO NOT EDIT! + +"$schema" = "../../schemas/schema.json" + +[[permission]] +identifier = "allow-stop-accessing-security-scoped-resource" +description = "Enables the stop_accessing_security_scoped_resource command without any pre-configured scope." +commands.allow = ["stop_accessing_security_scoped_resource"] + +[[permission]] +identifier = "deny-stop-accessing-security-scoped-resource" +description = "Denies the stop_accessing_security_scoped_resource command without any pre-configured scope." +commands.deny = ["stop_accessing_security_scoped_resource"] diff --git a/plugins/fs/permissions/autogenerated/commands/truncate.toml b/plugins/fs/permissions/autogenerated/commands/truncate.toml new file mode 100644 index 0000000..62fbb14 --- /dev/null +++ b/plugins/fs/permissions/autogenerated/commands/truncate.toml @@ -0,0 +1,13 @@ +# Automatically generated - DO NOT EDIT! + +"$schema" = "../../schemas/schema.json" + +[[permission]] +identifier = "allow-truncate" +description = "Enables the truncate command without any pre-configured scope." +commands.allow = ["truncate"] + +[[permission]] +identifier = "deny-truncate" +description = "Denies the truncate command without any pre-configured scope." +commands.deny = ["truncate"] diff --git a/plugins/fs/permissions/autogenerated/commands/unwatch.toml b/plugins/fs/permissions/autogenerated/commands/unwatch.toml new file mode 100644 index 0000000..3259e9a --- /dev/null +++ b/plugins/fs/permissions/autogenerated/commands/unwatch.toml @@ -0,0 +1,13 @@ +# Automatically generated - DO NOT EDIT! + +"$schema" = "../../schemas/schema.json" + +[[permission]] +identifier = "allow-unwatch" +description = "Enables the unwatch command without any pre-configured scope." +commands.allow = ["unwatch"] + +[[permission]] +identifier = "deny-unwatch" +description = "Denies the unwatch command without any pre-configured scope." +commands.deny = ["unwatch"] diff --git a/plugins/fs/permissions/autogenerated/commands/watch.toml b/plugins/fs/permissions/autogenerated/commands/watch.toml new file mode 100644 index 0000000..8dd1b57 --- /dev/null +++ b/plugins/fs/permissions/autogenerated/commands/watch.toml @@ -0,0 +1,13 @@ +# Automatically generated - DO NOT EDIT! + +"$schema" = "../../schemas/schema.json" + +[[permission]] +identifier = "allow-watch" +description = "Enables the watch command without any pre-configured scope." +commands.allow = ["watch"] + +[[permission]] +identifier = "deny-watch" +description = "Denies the watch command without any pre-configured scope." +commands.deny = ["watch"] diff --git a/plugins/fs/permissions/autogenerated/commands/write.toml b/plugins/fs/permissions/autogenerated/commands/write.toml new file mode 100644 index 0000000..73d1d38 --- /dev/null +++ b/plugins/fs/permissions/autogenerated/commands/write.toml @@ -0,0 +1,13 @@ +# Automatically generated - DO NOT EDIT! + +"$schema" = "../../schemas/schema.json" + +[[permission]] +identifier = "allow-write" +description = "Enables the write command without any pre-configured scope." +commands.allow = ["write"] + +[[permission]] +identifier = "deny-write" +description = "Denies the write command without any pre-configured scope." +commands.deny = ["write"] diff --git a/plugins/fs/permissions/autogenerated/commands/write_file.toml b/plugins/fs/permissions/autogenerated/commands/write_file.toml new file mode 100644 index 0000000..ea7d513 --- /dev/null +++ b/plugins/fs/permissions/autogenerated/commands/write_file.toml @@ -0,0 +1,23 @@ +# Automatically generated - DO NOT EDIT! + +"$schema" = "../../schemas/schema.json" + +[[permission]] +identifier = "allow-write-file" +description = "Enables the write_file command without any pre-configured scope." + +[permission.commands] +allow = [ + "write_file", + "open", + "write", +] +deny = [] + +[[permission]] +identifier = "deny-write-file" +description = "Denies the write_file command without any pre-configured scope." + +[permission.commands] +allow = [] +deny = ["write_file"] diff --git a/plugins/fs/permissions/autogenerated/commands/write_text_file.toml b/plugins/fs/permissions/autogenerated/commands/write_text_file.toml new file mode 100644 index 0000000..6b497a7 --- /dev/null +++ b/plugins/fs/permissions/autogenerated/commands/write_text_file.toml @@ -0,0 +1,13 @@ +# Automatically generated - DO NOT EDIT! + +"$schema" = "../../schemas/schema.json" + +[[permission]] +identifier = "allow-write-text-file" +description = "Enables the write_text_file command without any pre-configured scope." +commands.allow = ["write_text_file"] + +[[permission]] +identifier = "deny-write-text-file" +description = "Denies the write_text_file command without any pre-configured scope." +commands.deny = ["write_text_file"] diff --git a/plugins/fs/permissions/autogenerated/reference.md b/plugins/fs/permissions/autogenerated/reference.md new file mode 100644 index 0000000..49589f0 --- /dev/null +++ b/plugins/fs/permissions/autogenerated/reference.md @@ -0,0 +1,3866 @@ +## Default Permission + +This set of permissions describes the what kind of +file system access the `fs` plugin has enabled or denied by default. + +#### Granted Permissions + +This default permission set enables read access to the +application specific directories (AppConfig, AppData, AppLocalData, AppCache, +AppLog) and all files and sub directories created in it. +The location of these directories depends on the operating system, +where the application is run. + +In general these directories need to be manually created +by the application at runtime, before accessing files or folders +in it is possible. + +Therefore, it is also allowed to create all of these folders via +the `mkdir` command. + +#### Denied Permissions + +This default permission set prevents access to critical components +of the Tauri application by default. + +- On Windows the access to webview data folder `$APPLOCALDATA/EBWebView` is denied. +- On Linux the access to webview data paths inside `$APPLOCALDATA` are denied. + +#### This default permission set includes the following: + +- `create-app-specific-dirs` +- `read-app-specific-dirs-recursive` +- `deny-default` + +## Permission Table + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
IdentifierDescription
+ +`fs:allow-app-read-recursive` + + + +This allows full recursive read access to the complete application folders, files and subdirectories. + +
+ +`fs:allow-app-write-recursive` + + + +This allows full recursive write access to the complete application folders, files and subdirectories. + +
+ +`fs:allow-app-read` + + + +This allows non-recursive read access to the application folders. + +
+ +`fs:allow-app-write` + + + +This allows non-recursive write access to the application folders. + +
+ +`fs:allow-app-meta-recursive` + + + +This allows full recursive read access to metadata of the application folders, including file listing and statistics. + +
+ +`fs:allow-app-meta` + + + +This allows non-recursive read access to metadata of the application folders, including file listing and statistics. + +
+ +`fs:scope-app-recursive` + + + +This scope permits recursive access to the complete application folders, including sub directories and files. + +
+ +`fs:scope-app` + + + +This scope permits access to all files and list content of top level directories in the application folders. + +
+ +`fs:scope-app-index` + + + +This scope permits to list all files and folders in the application directories. + +
+ +`fs:allow-appcache-read-recursive` + + + +This allows full recursive read access to the complete `$APPCACHE` folder, files and subdirectories. + +
+ +`fs:allow-appcache-write-recursive` + + + +This allows full recursive write access to the complete `$APPCACHE` folder, files and subdirectories. + +
+ +`fs:allow-appcache-read` + + + +This allows non-recursive read access to the `$APPCACHE` folder. + +
+ +`fs:allow-appcache-write` + + + +This allows non-recursive write access to the `$APPCACHE` folder. + +
+ +`fs:allow-appcache-meta-recursive` + + + +This allows full recursive read access to metadata of the `$APPCACHE` folder, including file listing and statistics. + +
+ +`fs:allow-appcache-meta` + + + +This allows non-recursive read access to metadata of the `$APPCACHE` folder, including file listing and statistics. + +
+ +`fs:scope-appcache-recursive` + + + +This scope permits recursive access to the complete `$APPCACHE` folder, including sub directories and files. + +
+ +`fs:scope-appcache` + + + +This scope permits access to all files and list content of top level directories in the `$APPCACHE` folder. + +
+ +`fs:scope-appcache-index` + + + +This scope permits to list all files and folders in the `$APPCACHE`folder. + +
+ +`fs:allow-appconfig-read-recursive` + + + +This allows full recursive read access to the complete `$APPCONFIG` folder, files and subdirectories. + +
+ +`fs:allow-appconfig-write-recursive` + + + +This allows full recursive write access to the complete `$APPCONFIG` folder, files and subdirectories. + +
+ +`fs:allow-appconfig-read` + + + +This allows non-recursive read access to the `$APPCONFIG` folder. + +
+ +`fs:allow-appconfig-write` + + + +This allows non-recursive write access to the `$APPCONFIG` folder. + +
+ +`fs:allow-appconfig-meta-recursive` + + + +This allows full recursive read access to metadata of the `$APPCONFIG` folder, including file listing and statistics. + +
+ +`fs:allow-appconfig-meta` + + + +This allows non-recursive read access to metadata of the `$APPCONFIG` folder, including file listing and statistics. + +
+ +`fs:scope-appconfig-recursive` + + + +This scope permits recursive access to the complete `$APPCONFIG` folder, including sub directories and files. + +
+ +`fs:scope-appconfig` + + + +This scope permits access to all files and list content of top level directories in the `$APPCONFIG` folder. + +
+ +`fs:scope-appconfig-index` + + + +This scope permits to list all files and folders in the `$APPCONFIG`folder. + +
+ +`fs:allow-appdata-read-recursive` + + + +This allows full recursive read access to the complete `$APPDATA` folder, files and subdirectories. + +
+ +`fs:allow-appdata-write-recursive` + + + +This allows full recursive write access to the complete `$APPDATA` folder, files and subdirectories. + +
+ +`fs:allow-appdata-read` + + + +This allows non-recursive read access to the `$APPDATA` folder. + +
+ +`fs:allow-appdata-write` + + + +This allows non-recursive write access to the `$APPDATA` folder. + +
+ +`fs:allow-appdata-meta-recursive` + + + +This allows full recursive read access to metadata of the `$APPDATA` folder, including file listing and statistics. + +
+ +`fs:allow-appdata-meta` + + + +This allows non-recursive read access to metadata of the `$APPDATA` folder, including file listing and statistics. + +
+ +`fs:scope-appdata-recursive` + + + +This scope permits recursive access to the complete `$APPDATA` folder, including sub directories and files. + +
+ +`fs:scope-appdata` + + + +This scope permits access to all files and list content of top level directories in the `$APPDATA` folder. + +
+ +`fs:scope-appdata-index` + + + +This scope permits to list all files and folders in the `$APPDATA`folder. + +
+ +`fs:allow-applocaldata-read-recursive` + + + +This allows full recursive read access to the complete `$APPLOCALDATA` folder, files and subdirectories. + +
+ +`fs:allow-applocaldata-write-recursive` + + + +This allows full recursive write access to the complete `$APPLOCALDATA` folder, files and subdirectories. + +
+ +`fs:allow-applocaldata-read` + + + +This allows non-recursive read access to the `$APPLOCALDATA` folder. + +
+ +`fs:allow-applocaldata-write` + + + +This allows non-recursive write access to the `$APPLOCALDATA` folder. + +
+ +`fs:allow-applocaldata-meta-recursive` + + + +This allows full recursive read access to metadata of the `$APPLOCALDATA` folder, including file listing and statistics. + +
+ +`fs:allow-applocaldata-meta` + + + +This allows non-recursive read access to metadata of the `$APPLOCALDATA` folder, including file listing and statistics. + +
+ +`fs:scope-applocaldata-recursive` + + + +This scope permits recursive access to the complete `$APPLOCALDATA` folder, including sub directories and files. + +
+ +`fs:scope-applocaldata` + + + +This scope permits access to all files and list content of top level directories in the `$APPLOCALDATA` folder. + +
+ +`fs:scope-applocaldata-index` + + + +This scope permits to list all files and folders in the `$APPLOCALDATA`folder. + +
+ +`fs:allow-applog-read-recursive` + + + +This allows full recursive read access to the complete `$APPLOG` folder, files and subdirectories. + +
+ +`fs:allow-applog-write-recursive` + + + +This allows full recursive write access to the complete `$APPLOG` folder, files and subdirectories. + +
+ +`fs:allow-applog-read` + + + +This allows non-recursive read access to the `$APPLOG` folder. + +
+ +`fs:allow-applog-write` + + + +This allows non-recursive write access to the `$APPLOG` folder. + +
+ +`fs:allow-applog-meta-recursive` + + + +This allows full recursive read access to metadata of the `$APPLOG` folder, including file listing and statistics. + +
+ +`fs:allow-applog-meta` + + + +This allows non-recursive read access to metadata of the `$APPLOG` folder, including file listing and statistics. + +
+ +`fs:scope-applog-recursive` + + + +This scope permits recursive access to the complete `$APPLOG` folder, including sub directories and files. + +
+ +`fs:scope-applog` + + + +This scope permits access to all files and list content of top level directories in the `$APPLOG` folder. + +
+ +`fs:scope-applog-index` + + + +This scope permits to list all files and folders in the `$APPLOG`folder. + +
+ +`fs:allow-audio-read-recursive` + + + +This allows full recursive read access to the complete `$AUDIO` folder, files and subdirectories. + +
+ +`fs:allow-audio-write-recursive` + + + +This allows full recursive write access to the complete `$AUDIO` folder, files and subdirectories. + +
+ +`fs:allow-audio-read` + + + +This allows non-recursive read access to the `$AUDIO` folder. + +
+ +`fs:allow-audio-write` + + + +This allows non-recursive write access to the `$AUDIO` folder. + +
+ +`fs:allow-audio-meta-recursive` + + + +This allows full recursive read access to metadata of the `$AUDIO` folder, including file listing and statistics. + +
+ +`fs:allow-audio-meta` + + + +This allows non-recursive read access to metadata of the `$AUDIO` folder, including file listing and statistics. + +
+ +`fs:scope-audio-recursive` + + + +This scope permits recursive access to the complete `$AUDIO` folder, including sub directories and files. + +
+ +`fs:scope-audio` + + + +This scope permits access to all files and list content of top level directories in the `$AUDIO` folder. + +
+ +`fs:scope-audio-index` + + + +This scope permits to list all files and folders in the `$AUDIO`folder. + +
+ +`fs:allow-cache-read-recursive` + + + +This allows full recursive read access to the complete `$CACHE` folder, files and subdirectories. + +
+ +`fs:allow-cache-write-recursive` + + + +This allows full recursive write access to the complete `$CACHE` folder, files and subdirectories. + +
+ +`fs:allow-cache-read` + + + +This allows non-recursive read access to the `$CACHE` folder. + +
+ +`fs:allow-cache-write` + + + +This allows non-recursive write access to the `$CACHE` folder. + +
+ +`fs:allow-cache-meta-recursive` + + + +This allows full recursive read access to metadata of the `$CACHE` folder, including file listing and statistics. + +
+ +`fs:allow-cache-meta` + + + +This allows non-recursive read access to metadata of the `$CACHE` folder, including file listing and statistics. + +
+ +`fs:scope-cache-recursive` + + + +This scope permits recursive access to the complete `$CACHE` folder, including sub directories and files. + +
+ +`fs:scope-cache` + + + +This scope permits access to all files and list content of top level directories in the `$CACHE` folder. + +
+ +`fs:scope-cache-index` + + + +This scope permits to list all files and folders in the `$CACHE`folder. + +
+ +`fs:allow-config-read-recursive` + + + +This allows full recursive read access to the complete `$CONFIG` folder, files and subdirectories. + +
+ +`fs:allow-config-write-recursive` + + + +This allows full recursive write access to the complete `$CONFIG` folder, files and subdirectories. + +
+ +`fs:allow-config-read` + + + +This allows non-recursive read access to the `$CONFIG` folder. + +
+ +`fs:allow-config-write` + + + +This allows non-recursive write access to the `$CONFIG` folder. + +
+ +`fs:allow-config-meta-recursive` + + + +This allows full recursive read access to metadata of the `$CONFIG` folder, including file listing and statistics. + +
+ +`fs:allow-config-meta` + + + +This allows non-recursive read access to metadata of the `$CONFIG` folder, including file listing and statistics. + +
+ +`fs:scope-config-recursive` + + + +This scope permits recursive access to the complete `$CONFIG` folder, including sub directories and files. + +
+ +`fs:scope-config` + + + +This scope permits access to all files and list content of top level directories in the `$CONFIG` folder. + +
+ +`fs:scope-config-index` + + + +This scope permits to list all files and folders in the `$CONFIG`folder. + +
+ +`fs:allow-data-read-recursive` + + + +This allows full recursive read access to the complete `$DATA` folder, files and subdirectories. + +
+ +`fs:allow-data-write-recursive` + + + +This allows full recursive write access to the complete `$DATA` folder, files and subdirectories. + +
+ +`fs:allow-data-read` + + + +This allows non-recursive read access to the `$DATA` folder. + +
+ +`fs:allow-data-write` + + + +This allows non-recursive write access to the `$DATA` folder. + +
+ +`fs:allow-data-meta-recursive` + + + +This allows full recursive read access to metadata of the `$DATA` folder, including file listing and statistics. + +
+ +`fs:allow-data-meta` + + + +This allows non-recursive read access to metadata of the `$DATA` folder, including file listing and statistics. + +
+ +`fs:scope-data-recursive` + + + +This scope permits recursive access to the complete `$DATA` folder, including sub directories and files. + +
+ +`fs:scope-data` + + + +This scope permits access to all files and list content of top level directories in the `$DATA` folder. + +
+ +`fs:scope-data-index` + + + +This scope permits to list all files and folders in the `$DATA`folder. + +
+ +`fs:allow-desktop-read-recursive` + + + +This allows full recursive read access to the complete `$DESKTOP` folder, files and subdirectories. + +
+ +`fs:allow-desktop-write-recursive` + + + +This allows full recursive write access to the complete `$DESKTOP` folder, files and subdirectories. + +
+ +`fs:allow-desktop-read` + + + +This allows non-recursive read access to the `$DESKTOP` folder. + +
+ +`fs:allow-desktop-write` + + + +This allows non-recursive write access to the `$DESKTOP` folder. + +
+ +`fs:allow-desktop-meta-recursive` + + + +This allows full recursive read access to metadata of the `$DESKTOP` folder, including file listing and statistics. + +
+ +`fs:allow-desktop-meta` + + + +This allows non-recursive read access to metadata of the `$DESKTOP` folder, including file listing and statistics. + +
+ +`fs:scope-desktop-recursive` + + + +This scope permits recursive access to the complete `$DESKTOP` folder, including sub directories and files. + +
+ +`fs:scope-desktop` + + + +This scope permits access to all files and list content of top level directories in the `$DESKTOP` folder. + +
+ +`fs:scope-desktop-index` + + + +This scope permits to list all files and folders in the `$DESKTOP`folder. + +
+ +`fs:allow-document-read-recursive` + + + +This allows full recursive read access to the complete `$DOCUMENT` folder, files and subdirectories. + +
+ +`fs:allow-document-write-recursive` + + + +This allows full recursive write access to the complete `$DOCUMENT` folder, files and subdirectories. + +
+ +`fs:allow-document-read` + + + +This allows non-recursive read access to the `$DOCUMENT` folder. + +
+ +`fs:allow-document-write` + + + +This allows non-recursive write access to the `$DOCUMENT` folder. + +
+ +`fs:allow-document-meta-recursive` + + + +This allows full recursive read access to metadata of the `$DOCUMENT` folder, including file listing and statistics. + +
+ +`fs:allow-document-meta` + + + +This allows non-recursive read access to metadata of the `$DOCUMENT` folder, including file listing and statistics. + +
+ +`fs:scope-document-recursive` + + + +This scope permits recursive access to the complete `$DOCUMENT` folder, including sub directories and files. + +
+ +`fs:scope-document` + + + +This scope permits access to all files and list content of top level directories in the `$DOCUMENT` folder. + +
+ +`fs:scope-document-index` + + + +This scope permits to list all files and folders in the `$DOCUMENT`folder. + +
+ +`fs:allow-download-read-recursive` + + + +This allows full recursive read access to the complete `$DOWNLOAD` folder, files and subdirectories. + +
+ +`fs:allow-download-write-recursive` + + + +This allows full recursive write access to the complete `$DOWNLOAD` folder, files and subdirectories. + +
+ +`fs:allow-download-read` + + + +This allows non-recursive read access to the `$DOWNLOAD` folder. + +
+ +`fs:allow-download-write` + + + +This allows non-recursive write access to the `$DOWNLOAD` folder. + +
+ +`fs:allow-download-meta-recursive` + + + +This allows full recursive read access to metadata of the `$DOWNLOAD` folder, including file listing and statistics. + +
+ +`fs:allow-download-meta` + + + +This allows non-recursive read access to metadata of the `$DOWNLOAD` folder, including file listing and statistics. + +
+ +`fs:scope-download-recursive` + + + +This scope permits recursive access to the complete `$DOWNLOAD` folder, including sub directories and files. + +
+ +`fs:scope-download` + + + +This scope permits access to all files and list content of top level directories in the `$DOWNLOAD` folder. + +
+ +`fs:scope-download-index` + + + +This scope permits to list all files and folders in the `$DOWNLOAD`folder. + +
+ +`fs:allow-exe-read-recursive` + + + +This allows full recursive read access to the complete `$EXE` folder, files and subdirectories. + +
+ +`fs:allow-exe-write-recursive` + + + +This allows full recursive write access to the complete `$EXE` folder, files and subdirectories. + +
+ +`fs:allow-exe-read` + + + +This allows non-recursive read access to the `$EXE` folder. + +
+ +`fs:allow-exe-write` + + + +This allows non-recursive write access to the `$EXE` folder. + +
+ +`fs:allow-exe-meta-recursive` + + + +This allows full recursive read access to metadata of the `$EXE` folder, including file listing and statistics. + +
+ +`fs:allow-exe-meta` + + + +This allows non-recursive read access to metadata of the `$EXE` folder, including file listing and statistics. + +
+ +`fs:scope-exe-recursive` + + + +This scope permits recursive access to the complete `$EXE` folder, including sub directories and files. + +
+ +`fs:scope-exe` + + + +This scope permits access to all files and list content of top level directories in the `$EXE` folder. + +
+ +`fs:scope-exe-index` + + + +This scope permits to list all files and folders in the `$EXE`folder. + +
+ +`fs:allow-font-read-recursive` + + + +This allows full recursive read access to the complete `$FONT` folder, files and subdirectories. + +
+ +`fs:allow-font-write-recursive` + + + +This allows full recursive write access to the complete `$FONT` folder, files and subdirectories. + +
+ +`fs:allow-font-read` + + + +This allows non-recursive read access to the `$FONT` folder. + +
+ +`fs:allow-font-write` + + + +This allows non-recursive write access to the `$FONT` folder. + +
+ +`fs:allow-font-meta-recursive` + + + +This allows full recursive read access to metadata of the `$FONT` folder, including file listing and statistics. + +
+ +`fs:allow-font-meta` + + + +This allows non-recursive read access to metadata of the `$FONT` folder, including file listing and statistics. + +
+ +`fs:scope-font-recursive` + + + +This scope permits recursive access to the complete `$FONT` folder, including sub directories and files. + +
+ +`fs:scope-font` + + + +This scope permits access to all files and list content of top level directories in the `$FONT` folder. + +
+ +`fs:scope-font-index` + + + +This scope permits to list all files and folders in the `$FONT`folder. + +
+ +`fs:allow-home-read-recursive` + + + +This allows full recursive read access to the complete `$HOME` folder, files and subdirectories. + +
+ +`fs:allow-home-write-recursive` + + + +This allows full recursive write access to the complete `$HOME` folder, files and subdirectories. + +
+ +`fs:allow-home-read` + + + +This allows non-recursive read access to the `$HOME` folder. + +
+ +`fs:allow-home-write` + + + +This allows non-recursive write access to the `$HOME` folder. + +
+ +`fs:allow-home-meta-recursive` + + + +This allows full recursive read access to metadata of the `$HOME` folder, including file listing and statistics. + +
+ +`fs:allow-home-meta` + + + +This allows non-recursive read access to metadata of the `$HOME` folder, including file listing and statistics. + +
+ +`fs:scope-home-recursive` + + + +This scope permits recursive access to the complete `$HOME` folder, including sub directories and files. + +
+ +`fs:scope-home` + + + +This scope permits access to all files and list content of top level directories in the `$HOME` folder. + +
+ +`fs:scope-home-index` + + + +This scope permits to list all files and folders in the `$HOME`folder. + +
+ +`fs:allow-localdata-read-recursive` + + + +This allows full recursive read access to the complete `$LOCALDATA` folder, files and subdirectories. + +
+ +`fs:allow-localdata-write-recursive` + + + +This allows full recursive write access to the complete `$LOCALDATA` folder, files and subdirectories. + +
+ +`fs:allow-localdata-read` + + + +This allows non-recursive read access to the `$LOCALDATA` folder. + +
+ +`fs:allow-localdata-write` + + + +This allows non-recursive write access to the `$LOCALDATA` folder. + +
+ +`fs:allow-localdata-meta-recursive` + + + +This allows full recursive read access to metadata of the `$LOCALDATA` folder, including file listing and statistics. + +
+ +`fs:allow-localdata-meta` + + + +This allows non-recursive read access to metadata of the `$LOCALDATA` folder, including file listing and statistics. + +
+ +`fs:scope-localdata-recursive` + + + +This scope permits recursive access to the complete `$LOCALDATA` folder, including sub directories and files. + +
+ +`fs:scope-localdata` + + + +This scope permits access to all files and list content of top level directories in the `$LOCALDATA` folder. + +
+ +`fs:scope-localdata-index` + + + +This scope permits to list all files and folders in the `$LOCALDATA`folder. + +
+ +`fs:allow-log-read-recursive` + + + +This allows full recursive read access to the complete `$LOG` folder, files and subdirectories. + +
+ +`fs:allow-log-write-recursive` + + + +This allows full recursive write access to the complete `$LOG` folder, files and subdirectories. + +
+ +`fs:allow-log-read` + + + +This allows non-recursive read access to the `$LOG` folder. + +
+ +`fs:allow-log-write` + + + +This allows non-recursive write access to the `$LOG` folder. + +
+ +`fs:allow-log-meta-recursive` + + + +This allows full recursive read access to metadata of the `$LOG` folder, including file listing and statistics. + +
+ +`fs:allow-log-meta` + + + +This allows non-recursive read access to metadata of the `$LOG` folder, including file listing and statistics. + +
+ +`fs:scope-log-recursive` + + + +This scope permits recursive access to the complete `$LOG` folder, including sub directories and files. + +
+ +`fs:scope-log` + + + +This scope permits access to all files and list content of top level directories in the `$LOG` folder. + +
+ +`fs:scope-log-index` + + + +This scope permits to list all files and folders in the `$LOG`folder. + +
+ +`fs:allow-picture-read-recursive` + + + +This allows full recursive read access to the complete `$PICTURE` folder, files and subdirectories. + +
+ +`fs:allow-picture-write-recursive` + + + +This allows full recursive write access to the complete `$PICTURE` folder, files and subdirectories. + +
+ +`fs:allow-picture-read` + + + +This allows non-recursive read access to the `$PICTURE` folder. + +
+ +`fs:allow-picture-write` + + + +This allows non-recursive write access to the `$PICTURE` folder. + +
+ +`fs:allow-picture-meta-recursive` + + + +This allows full recursive read access to metadata of the `$PICTURE` folder, including file listing and statistics. + +
+ +`fs:allow-picture-meta` + + + +This allows non-recursive read access to metadata of the `$PICTURE` folder, including file listing and statistics. + +
+ +`fs:scope-picture-recursive` + + + +This scope permits recursive access to the complete `$PICTURE` folder, including sub directories and files. + +
+ +`fs:scope-picture` + + + +This scope permits access to all files and list content of top level directories in the `$PICTURE` folder. + +
+ +`fs:scope-picture-index` + + + +This scope permits to list all files and folders in the `$PICTURE`folder. + +
+ +`fs:allow-public-read-recursive` + + + +This allows full recursive read access to the complete `$PUBLIC` folder, files and subdirectories. + +
+ +`fs:allow-public-write-recursive` + + + +This allows full recursive write access to the complete `$PUBLIC` folder, files and subdirectories. + +
+ +`fs:allow-public-read` + + + +This allows non-recursive read access to the `$PUBLIC` folder. + +
+ +`fs:allow-public-write` + + + +This allows non-recursive write access to the `$PUBLIC` folder. + +
+ +`fs:allow-public-meta-recursive` + + + +This allows full recursive read access to metadata of the `$PUBLIC` folder, including file listing and statistics. + +
+ +`fs:allow-public-meta` + + + +This allows non-recursive read access to metadata of the `$PUBLIC` folder, including file listing and statistics. + +
+ +`fs:scope-public-recursive` + + + +This scope permits recursive access to the complete `$PUBLIC` folder, including sub directories and files. + +
+ +`fs:scope-public` + + + +This scope permits access to all files and list content of top level directories in the `$PUBLIC` folder. + +
+ +`fs:scope-public-index` + + + +This scope permits to list all files and folders in the `$PUBLIC`folder. + +
+ +`fs:allow-resource-read-recursive` + + + +This allows full recursive read access to the complete `$RESOURCE` folder, files and subdirectories. + +
+ +`fs:allow-resource-write-recursive` + + + +This allows full recursive write access to the complete `$RESOURCE` folder, files and subdirectories. + +
+ +`fs:allow-resource-read` + + + +This allows non-recursive read access to the `$RESOURCE` folder. + +
+ +`fs:allow-resource-write` + + + +This allows non-recursive write access to the `$RESOURCE` folder. + +
+ +`fs:allow-resource-meta-recursive` + + + +This allows full recursive read access to metadata of the `$RESOURCE` folder, including file listing and statistics. + +
+ +`fs:allow-resource-meta` + + + +This allows non-recursive read access to metadata of the `$RESOURCE` folder, including file listing and statistics. + +
+ +`fs:scope-resource-recursive` + + + +This scope permits recursive access to the complete `$RESOURCE` folder, including sub directories and files. + +
+ +`fs:scope-resource` + + + +This scope permits access to all files and list content of top level directories in the `$RESOURCE` folder. + +
+ +`fs:scope-resource-index` + + + +This scope permits to list all files and folders in the `$RESOURCE`folder. + +
+ +`fs:allow-runtime-read-recursive` + + + +This allows full recursive read access to the complete `$RUNTIME` folder, files and subdirectories. + +
+ +`fs:allow-runtime-write-recursive` + + + +This allows full recursive write access to the complete `$RUNTIME` folder, files and subdirectories. + +
+ +`fs:allow-runtime-read` + + + +This allows non-recursive read access to the `$RUNTIME` folder. + +
+ +`fs:allow-runtime-write` + + + +This allows non-recursive write access to the `$RUNTIME` folder. + +
+ +`fs:allow-runtime-meta-recursive` + + + +This allows full recursive read access to metadata of the `$RUNTIME` folder, including file listing and statistics. + +
+ +`fs:allow-runtime-meta` + + + +This allows non-recursive read access to metadata of the `$RUNTIME` folder, including file listing and statistics. + +
+ +`fs:scope-runtime-recursive` + + + +This scope permits recursive access to the complete `$RUNTIME` folder, including sub directories and files. + +
+ +`fs:scope-runtime` + + + +This scope permits access to all files and list content of top level directories in the `$RUNTIME` folder. + +
+ +`fs:scope-runtime-index` + + + +This scope permits to list all files and folders in the `$RUNTIME`folder. + +
+ +`fs:allow-temp-read-recursive` + + + +This allows full recursive read access to the complete `$TEMP` folder, files and subdirectories. + +
+ +`fs:allow-temp-write-recursive` + + + +This allows full recursive write access to the complete `$TEMP` folder, files and subdirectories. + +
+ +`fs:allow-temp-read` + + + +This allows non-recursive read access to the `$TEMP` folder. + +
+ +`fs:allow-temp-write` + + + +This allows non-recursive write access to the `$TEMP` folder. + +
+ +`fs:allow-temp-meta-recursive` + + + +This allows full recursive read access to metadata of the `$TEMP` folder, including file listing and statistics. + +
+ +`fs:allow-temp-meta` + + + +This allows non-recursive read access to metadata of the `$TEMP` folder, including file listing and statistics. + +
+ +`fs:scope-temp-recursive` + + + +This scope permits recursive access to the complete `$TEMP` folder, including sub directories and files. + +
+ +`fs:scope-temp` + + + +This scope permits access to all files and list content of top level directories in the `$TEMP` folder. + +
+ +`fs:scope-temp-index` + + + +This scope permits to list all files and folders in the `$TEMP`folder. + +
+ +`fs:allow-template-read-recursive` + + + +This allows full recursive read access to the complete `$TEMPLATE` folder, files and subdirectories. + +
+ +`fs:allow-template-write-recursive` + + + +This allows full recursive write access to the complete `$TEMPLATE` folder, files and subdirectories. + +
+ +`fs:allow-template-read` + + + +This allows non-recursive read access to the `$TEMPLATE` folder. + +
+ +`fs:allow-template-write` + + + +This allows non-recursive write access to the `$TEMPLATE` folder. + +
+ +`fs:allow-template-meta-recursive` + + + +This allows full recursive read access to metadata of the `$TEMPLATE` folder, including file listing and statistics. + +
+ +`fs:allow-template-meta` + + + +This allows non-recursive read access to metadata of the `$TEMPLATE` folder, including file listing and statistics. + +
+ +`fs:scope-template-recursive` + + + +This scope permits recursive access to the complete `$TEMPLATE` folder, including sub directories and files. + +
+ +`fs:scope-template` + + + +This scope permits access to all files and list content of top level directories in the `$TEMPLATE` folder. + +
+ +`fs:scope-template-index` + + + +This scope permits to list all files and folders in the `$TEMPLATE`folder. + +
+ +`fs:allow-video-read-recursive` + + + +This allows full recursive read access to the complete `$VIDEO` folder, files and subdirectories. + +
+ +`fs:allow-video-write-recursive` + + + +This allows full recursive write access to the complete `$VIDEO` folder, files and subdirectories. + +
+ +`fs:allow-video-read` + + + +This allows non-recursive read access to the `$VIDEO` folder. + +
+ +`fs:allow-video-write` + + + +This allows non-recursive write access to the `$VIDEO` folder. + +
+ +`fs:allow-video-meta-recursive` + + + +This allows full recursive read access to metadata of the `$VIDEO` folder, including file listing and statistics. + +
+ +`fs:allow-video-meta` + + + +This allows non-recursive read access to metadata of the `$VIDEO` folder, including file listing and statistics. + +
+ +`fs:scope-video-recursive` + + + +This scope permits recursive access to the complete `$VIDEO` folder, including sub directories and files. + +
+ +`fs:scope-video` + + + +This scope permits access to all files and list content of top level directories in the `$VIDEO` folder. + +
+ +`fs:scope-video-index` + + + +This scope permits to list all files and folders in the `$VIDEO`folder. + +
+ +`fs:allow-copy-file` + + + +Enables the copy_file command without any pre-configured scope. + +
+ +`fs:deny-copy-file` + + + +Denies the copy_file command without any pre-configured scope. + +
+ +`fs:allow-create` + + + +Enables the create command without any pre-configured scope. + +
+ +`fs:deny-create` + + + +Denies the create command without any pre-configured scope. + +
+ +`fs:allow-exists` + + + +Enables the exists command without any pre-configured scope. + +
+ +`fs:deny-exists` + + + +Denies the exists command without any pre-configured scope. + +
+ +`fs:allow-fstat` + + + +Enables the fstat command without any pre-configured scope. + +
+ +`fs:deny-fstat` + + + +Denies the fstat command without any pre-configured scope. + +
+ +`fs:allow-ftruncate` + + + +Enables the ftruncate command without any pre-configured scope. + +
+ +`fs:deny-ftruncate` + + + +Denies the ftruncate command without any pre-configured scope. + +
+ +`fs:allow-lstat` + + + +Enables the lstat command without any pre-configured scope. + +
+ +`fs:deny-lstat` + + + +Denies the lstat command without any pre-configured scope. + +
+ +`fs:allow-mkdir` + + + +Enables the mkdir command without any pre-configured scope. + +
+ +`fs:deny-mkdir` + + + +Denies the mkdir command without any pre-configured scope. + +
+ +`fs:allow-open` + + + +Enables the open command without any pre-configured scope. + +
+ +`fs:deny-open` + + + +Denies the open command without any pre-configured scope. + +
+ +`fs:allow-read` + + + +Enables the read command without any pre-configured scope. + +
+ +`fs:deny-read` + + + +Denies the read command without any pre-configured scope. + +
+ +`fs:allow-read-dir` + + + +Enables the read_dir command without any pre-configured scope. + +
+ +`fs:deny-read-dir` + + + +Denies the read_dir command without any pre-configured scope. + +
+ +`fs:allow-read-file` + + + +Enables the read_file command without any pre-configured scope. + +
+ +`fs:deny-read-file` + + + +Denies the read_file command without any pre-configured scope. + +
+ +`fs:allow-read-text-file` + + + +Enables the read_text_file command without any pre-configured scope. + +
+ +`fs:deny-read-text-file` + + + +Denies the read_text_file command without any pre-configured scope. + +
+ +`fs:allow-read-text-file-lines` + + + +Enables the read_text_file_lines command without any pre-configured scope. + +
+ +`fs:deny-read-text-file-lines` + + + +Denies the read_text_file_lines command without any pre-configured scope. + +
+ +`fs:allow-read-text-file-lines-next` + + + +Enables the read_text_file_lines_next command without any pre-configured scope. + +
+ +`fs:deny-read-text-file-lines-next` + + + +Denies the read_text_file_lines_next command without any pre-configured scope. + +
+ +`fs:allow-remove` + + + +Enables the remove command without any pre-configured scope. + +
+ +`fs:deny-remove` + + + +Denies the remove command without any pre-configured scope. + +
+ +`fs:allow-rename` + + + +Enables the rename command without any pre-configured scope. + +
+ +`fs:deny-rename` + + + +Denies the rename command without any pre-configured scope. + +
+ +`fs:allow-seek` + + + +Enables the seek command without any pre-configured scope. + +
+ +`fs:deny-seek` + + + +Denies the seek command without any pre-configured scope. + +
+ +`fs:allow-size` + + + +Enables the size command without any pre-configured scope. + +
+ +`fs:deny-size` + + + +Denies the size command without any pre-configured scope. + +
+ +`fs:allow-start-accessing-security-scoped-resource` + + + +Enables the start_accessing_security_scoped_resource command without any pre-configured scope. + +
+ +`fs:deny-start-accessing-security-scoped-resource` + + + +Denies the start_accessing_security_scoped_resource command without any pre-configured scope. + +
+ +`fs:allow-stat` + + + +Enables the stat command without any pre-configured scope. + +
+ +`fs:deny-stat` + + + +Denies the stat command without any pre-configured scope. + +
+ +`fs:allow-stop-accessing-security-scoped-resource` + + + +Enables the stop_accessing_security_scoped_resource command without any pre-configured scope. + +
+ +`fs:deny-stop-accessing-security-scoped-resource` + + + +Denies the stop_accessing_security_scoped_resource command without any pre-configured scope. + +
+ +`fs:allow-truncate` + + + +Enables the truncate command without any pre-configured scope. + +
+ +`fs:deny-truncate` + + + +Denies the truncate command without any pre-configured scope. + +
+ +`fs:allow-unwatch` + + + +Enables the unwatch command without any pre-configured scope. + +
+ +`fs:deny-unwatch` + + + +Denies the unwatch command without any pre-configured scope. + +
+ +`fs:allow-watch` + + + +Enables the watch command without any pre-configured scope. + +
+ +`fs:deny-watch` + + + +Denies the watch command without any pre-configured scope. + +
+ +`fs:allow-write` + + + +Enables the write command without any pre-configured scope. + +
+ +`fs:deny-write` + + + +Denies the write command without any pre-configured scope. + +
+ +`fs:allow-write-file` + + + +Enables the write_file command without any pre-configured scope. + +
+ +`fs:deny-write-file` + + + +Denies the write_file command without any pre-configured scope. + +
+ +`fs:allow-write-text-file` + + + +Enables the write_text_file command without any pre-configured scope. + +
+ +`fs:deny-write-text-file` + + + +Denies the write_text_file command without any pre-configured scope. + +
+ +`fs:create-app-specific-dirs` + + + +This permissions allows to create the application specific directories. + + +
+ +`fs:deny-default` + + + +This denies access to dangerous Tauri relevant files and folders by default. + +
+ +`fs:deny-webview-data-linux` + + + +This denies access to the following paths + +- `$APPLOCALDATA/CacheStorage/**` +- `$APPLOCALDATA/cookies` +- `$APPLOCALDATA/hsts-storage.sqlite` +- `$APPLOCALDATA/serviceworkers/**` +- `$APPLOCALDATA/WebKitCache/**` +- `$APPLOCALDATA/databases/**` +- `$APPLOCALDATA/localstorage/**` +- `$APPLOCALDATA/mediakeys/**` +- `$APPLOCALDATA/storage/**` + +on linux as the webview data and configuration values are stored here. +Allowing access can lead to sensitive information disclosure and should be well considered. + +
+ +`fs:deny-webview-data-windows` + + + +This denies access to the +`$APPLOCALDATA/EBWebView` folder on windows as the webview data and configuration values are stored here. +Allowing access can lead to sensitive information disclosure and should be well considered. + +
+ +`fs:read-all` + + + +This enables all read related commands without any pre-configured accessible paths. + +
+ +`fs:read-app-specific-dirs-recursive` + + + +This permission allows recursive read functionality on the application +specific base directories. + + +
+ +`fs:read-dirs` + + + +This enables directory read and file metadata related commands without any pre-configured accessible paths. + +
+ +`fs:read-files` + + + +This enables file read related commands without any pre-configured accessible paths. + +
+ +`fs:read-meta` + + + +This enables all index or metadata related commands without any pre-configured accessible paths. + +
+ +`fs:scope` + + + +An empty permission you can use to modify the global scope. + +## Example + +```json +{ + "identifier": "read-documents", + "windows": ["main"], + "permissions": [ + "fs:allow-read", + { + "identifier": "fs:scope", + "allow": [ + "$APPDATA/documents/**/*" + ], + "deny": [ + "$APPDATA/documents/secret.txt" + ] + } + ] +} +``` + + +
+ +`fs:write-all` + + + +This enables all write related commands without any pre-configured accessible paths. + +
+ +`fs:write-files` + + + +This enables all file write related commands without any pre-configured accessible paths. + +
diff --git a/plugins/fs/permissions/create-app-specific-dirs.toml b/plugins/fs/permissions/create-app-specific-dirs.toml new file mode 100644 index 0000000..3c72293 --- /dev/null +++ b/plugins/fs/permissions/create-app-specific-dirs.toml @@ -0,0 +1,8 @@ +"$schema" = "schemas/schema.json" + +[[set]] +identifier = "create-app-specific-dirs" +description = """ +This permissions allows to create the application specific directories. +""" +permissions = ["allow-mkdir", "scope-app-index"] diff --git a/plugins/fs/permissions/default.toml b/plugins/fs/permissions/default.toml new file mode 100644 index 0000000..2afcc7d --- /dev/null +++ b/plugins/fs/permissions/default.toml @@ -0,0 +1,35 @@ +"$schema" = "schemas/schema.json" + +[default] +description = """ +This set of permissions describes the what kind of +file system access the `fs` plugin has enabled or denied by default. + +#### Granted Permissions + +This default permission set enables read access to the +application specific directories (AppConfig, AppData, AppLocalData, AppCache, +AppLog) and all files and sub directories created in it. +The location of these directories depends on the operating system, +where the application is run. + +In general these directories need to be manually created +by the application at runtime, before accessing files or folders +in it is possible. + +Therefore, it is also allowed to create all of these folders via +the `mkdir` command. + +#### Denied Permissions + +This default permission set prevents access to critical components +of the Tauri application by default. + +- On Windows the access to webview data folder `$APPLOCALDATA/EBWebView` is denied. +- On Linux the access to webview data paths inside `$APPLOCALDATA` are denied. +""" +permissions = [ + "create-app-specific-dirs", + "read-app-specific-dirs-recursive", + "deny-default", +] diff --git a/plugins/fs/permissions/deny-default.toml b/plugins/fs/permissions/deny-default.toml new file mode 100644 index 0000000..22d8186 --- /dev/null +++ b/plugins/fs/permissions/deny-default.toml @@ -0,0 +1,6 @@ +"$schema" = "schemas/schema.json" + +[[set]] +identifier = "deny-default" +description = "This denies access to dangerous Tauri relevant files and folders by default." +permissions = ["deny-webview-data-linux", "deny-webview-data-windows"] diff --git a/plugins/fs/permissions/deny-webview-data.toml b/plugins/fs/permissions/deny-webview-data.toml new file mode 100644 index 0000000..eec5991 --- /dev/null +++ b/plugins/fs/permissions/deny-webview-data.toml @@ -0,0 +1,48 @@ +"$schema" = "schemas/schema.json" + +[[permission]] +identifier = "deny-webview-data-linux" +description = """This denies access to the following paths + +- `$APPLOCALDATA/CacheStorage/**` +- `$APPLOCALDATA/cookies` +- `$APPLOCALDATA/hsts-storage.sqlite` +- `$APPLOCALDATA/serviceworkers/**` +- `$APPLOCALDATA/WebKitCache/**` +- `$APPLOCALDATA/databases/**` +- `$APPLOCALDATA/localstorage/**` +- `$APPLOCALDATA/mediakeys/**` +- `$APPLOCALDATA/storage/**` + +on linux as the webview data and configuration values are stored here. +Allowing access can lead to sensitive information disclosure and should be well considered.""" +platforms = ["linux"] + +[[permission.scope.deny]] +path = "$APPLOCALDATA/CacheStorage/**" +[[permission.scope.deny]] +path = "$APPLOCALDATA/cookies" +[[permission.scope.deny]] +path = "$APPLOCALDATA/hsts-storage.sqlite" +[[permission.scope.deny]] +path = "$APPLOCALDATA/serviceworkers/**" +[[permission.scope.deny]] +path = "$APPLOCALDATA/WebKitCache/**" +[[permission.scope.deny]] +path = "$APPLOCALDATA/databases/**" +[[permission.scope.deny]] +path = "$APPLOCALDATA/localstorage/**" +[[permission.scope.deny]] +path = "$APPLOCALDATA/mediakeys/**" +[[permission.scope.deny]] +path = "$APPLOCALDATA/storage/**" + +[[permission]] +identifier = "deny-webview-data-windows" +description = """This denies access to the +`$APPLOCALDATA/EBWebView` folder on windows as the webview data and configuration values are stored here. +Allowing access can lead to sensitive information disclosure and should be well considered.""" +platforms = ["windows"] + +[[permission.scope.deny]] +path = "$APPLOCALDATA/EBWebView/**" diff --git a/plugins/fs/permissions/read-all.toml b/plugins/fs/permissions/read-all.toml new file mode 100644 index 0000000..d43af5e --- /dev/null +++ b/plugins/fs/permissions/read-all.toml @@ -0,0 +1,21 @@ +"$schema" = "schemas/schema.json" + +[[permission]] +identifier = "read-all" +description = "This enables all read related commands without any pre-configured accessible paths." +commands.allow = [ + "read_dir", + "read_file", + "read", + "open", + "read_text_file", + "read_text_file_lines", + "read_text_file_lines_next", + "seek", + "stat", + "lstat", + "fstat", + "exists", + "watch", + "unwatch", +] diff --git a/plugins/fs/permissions/read-app-specific-dirs-recursive.toml b/plugins/fs/permissions/read-app-specific-dirs-recursive.toml new file mode 100644 index 0000000..8da8c1d --- /dev/null +++ b/plugins/fs/permissions/read-app-specific-dirs-recursive.toml @@ -0,0 +1,17 @@ +"$schema" = "schemas/schema.json" + +[[set]] +identifier = "read-app-specific-dirs-recursive" +description = """ +This permission allows recursive read functionality on the application +specific base directories. +""" +permissions = [ + "allow-read-dir", + "allow-read-file", + "allow-read-text-file", + "allow-read-text-file-lines", + "allow-read-text-file-lines-next", + "allow-exists", + "scope-app-recursive", +] diff --git a/plugins/fs/permissions/read-dirs.toml b/plugins/fs/permissions/read-dirs.toml new file mode 100644 index 0000000..eb38363 --- /dev/null +++ b/plugins/fs/permissions/read-dirs.toml @@ -0,0 +1,6 @@ +"$schema" = "schemas/schema.json" + +[[permission]] +identifier = "read-dirs" +description = "This enables directory read and file metadata related commands without any pre-configured accessible paths." +commands.allow = ["read_dir", "stat", "lstat", "fstat", "exists"] diff --git a/plugins/fs/permissions/read-files.toml b/plugins/fs/permissions/read-files.toml new file mode 100644 index 0000000..f268510 --- /dev/null +++ b/plugins/fs/permissions/read-files.toml @@ -0,0 +1,19 @@ +"$schema" = "schemas/schema.json" + +[[permission]] +identifier = "read-files" +description = "This enables file read related commands without any pre-configured accessible paths." +commands.allow = [ + "read_file", + "read", + "open", + "read_text_file", + "read_text_file_lines", + "read_text_file_lines_next", + "seek", + "stat", + "lstat", + "fstat", + "exists", + +] diff --git a/plugins/fs/permissions/read-meta.toml b/plugins/fs/permissions/read-meta.toml new file mode 100644 index 0000000..83024b0 --- /dev/null +++ b/plugins/fs/permissions/read-meta.toml @@ -0,0 +1,6 @@ +"$schema" = "schemas/schema.json" + +[[permission]] +identifier = "read-meta" +description = "This enables all index or metadata related commands without any pre-configured accessible paths." +commands.allow = ["read_dir", "stat", "lstat", "fstat", "exists", "size"] diff --git a/plugins/fs/permissions/scope.toml b/plugins/fs/permissions/scope.toml new file mode 100644 index 0000000..03ba275 --- /dev/null +++ b/plugins/fs/permissions/scope.toml @@ -0,0 +1,28 @@ +"$schema" = "schemas/schema.json" + +[[permission]] +identifier = "scope" +description = """ +An empty permission you can use to modify the global scope. + +## Example + +```json +{ + "identifier": "read-documents", + "windows": ["main"], + "permissions": [ + "fs:allow-read", + { + "identifier": "fs:scope", + "allow": [ + "$APPDATA/documents/**/*" + ], + "deny": [ + "$APPDATA/documents/secret.txt" + ] + } + ] +} +``` +""" diff --git a/plugins/fs/permissions/write-all.toml b/plugins/fs/permissions/write-all.toml new file mode 100644 index 0000000..c180278 --- /dev/null +++ b/plugins/fs/permissions/write-all.toml @@ -0,0 +1,17 @@ +"$schema" = "schemas/schema.json" + +[[permission]] +identifier = "write-all" +description = "This enables all write related commands without any pre-configured accessible paths." +commands.allow = [ + "mkdir", + "create", + "copy_file", + "remove", + "rename", + "truncate", + "ftruncate", + "write", + "write_file", + "write_text_file", +] diff --git a/plugins/fs/permissions/write-files.toml b/plugins/fs/permissions/write-files.toml new file mode 100644 index 0000000..2d6aeff --- /dev/null +++ b/plugins/fs/permissions/write-files.toml @@ -0,0 +1,16 @@ +"$schema" = "schemas/schema.json" + +[[permission]] +identifier = "write-files" +description = "This enables all file write related commands without any pre-configured accessible paths." +commands.allow = [ + "create", + "copy_file", + "remove", + "rename", + "truncate", + "ftruncate", + "write", + "write_file", + "write_text_file", +] diff --git a/plugins/fs/rollup.config.js b/plugins/fs/rollup.config.js new file mode 100644 index 0000000..1f349ec --- /dev/null +++ b/plugins/fs/rollup.config.js @@ -0,0 +1,7 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +import { createConfig } from '../../shared/rollup.config.js' + +export default createConfig() diff --git a/plugins/fs/src/android.rs b/plugins/fs/src/android.rs new file mode 100644 index 0000000..d00b4fc --- /dev/null +++ b/plugins/fs/src/android.rs @@ -0,0 +1,92 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use serde::de::DeserializeOwned; +use tauri::{plugin::PluginApi, AppHandle, Runtime}; + +use crate::{models::*, FilePath, OpenOptions}; + +const PLUGIN_IDENTIFIER: &str = "com.plugin.fs"; + +/// Access to the file system APIs on Android. +/// +/// In addition to regular file system paths, it can read `content://` URIs +/// and Android asset paths by resolving them with the Android plugin implementation. +/// +/// Retrieved with [`crate::FsExt::fs`]. +pub struct Fs(tauri::plugin::PluginHandle); + +pub fn init( + _app: &AppHandle, + api: PluginApi, +) -> crate::Result> { + let handle = api + .register_android_plugin(PLUGIN_IDENTIFIER, "FsPlugin") + .unwrap(); + Ok(Fs(handle)) +} + +impl Fs { + /// Open a file. + /// + /// # Platform-specific + /// + /// - **iOS**: This method will automatically start accessing a security-scoped resource if the path is a file URL. + /// You must call `stop_accessing_security_scoped_resource` when you're done accessing the file. + pub fn open>( + &self, + path: P, + opts: OpenOptions, + ) -> std::io::Result { + match path.into() { + FilePath::Url(u) => self + .resolve_content_uri(u.to_string(), opts.android_mode()) + .map_err(|e| { + std::io::Error::new( + std::io::ErrorKind::Other, + format!("failed to open file: {e}"), + ) + }), + FilePath::Path(p) => { + // tauri::utils::platform::resources_dir() returns a PathBuf with the Android asset URI prefix + // we must resolve that file with the Android API + if p.strip_prefix(tauri::utils::platform::ANDROID_ASSET_PROTOCOL_URI_PREFIX) + .is_ok() + { + self.resolve_content_uri(p.to_string_lossy(), opts.android_mode()) + .map_err(|e| { + std::io::Error::new( + std::io::ErrorKind::Other, + format!("failed to open file: {e}"), + ) + }) + } else { + std::fs::OpenOptions::from(opts).open(p) + } + } + } + } + + fn resolve_content_uri( + &self, + uri: impl Into, + mode: impl Into, + ) -> crate::Result { + let result = self.0.run_mobile_plugin::( + "getFileDescriptor", + GetFileDescriptorPayload { + uri: uri.into(), + mode: mode.into(), + }, + )?; + if let Some(fd) = result.fd { + Ok(unsafe { + use std::os::fd::FromRawFd; + std::fs::File::from_raw_fd(fd) + }) + } else { + unimplemented!() + } + } +} diff --git a/plugins/fs/src/commands.rs b/plugins/fs/src/commands.rs new file mode 100644 index 0000000..fcc38f6 --- /dev/null +++ b/plugins/fs/src/commands.rs @@ -0,0 +1,1885 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// Copyright 2018-2023 the Deno authors. +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use serde::{Deserialize, Serialize, Serializer}; +use serde_repr::{Deserialize_repr, Serialize_repr}; +use tauri::{ + ipc::{CommandScope, GlobalScope}, + path::BaseDirectory, + utils::config::FsScope, + Manager, Resource, ResourceId, Runtime, Webview, +}; + +use std::{ + borrow::Cow, + fs::File, + io::{BufRead, BufReader, Read, Write}, + ops::{Deref, DerefMut}, + path::{Path, PathBuf}, + str::FromStr, + sync::Mutex, + time::{SystemTime, UNIX_EPOCH}, +}; + +use crate::{scope::Entry, Error, SafeFilePath}; + +#[derive(Debug, thiserror::Error)] +pub enum CommandError { + #[error(transparent)] + Anyhow(#[from] anyhow::Error), + #[error(transparent)] + Plugin(#[from] Error), + #[error(transparent)] + Tauri(#[from] tauri::Error), + #[error(transparent)] + Json(#[from] serde_json::Error), + #[error(transparent)] + Io(#[from] std::io::Error), + #[error(transparent)] + UrlParseError(#[from] url::ParseError), + #[cfg(feature = "watch")] + #[error(transparent)] + Watcher(#[from] notify::Error), +} + +impl From for CommandError { + fn from(value: String) -> Self { + Self::Anyhow(anyhow::anyhow!(value)) + } +} + +impl From<&str> for CommandError { + fn from(value: &str) -> Self { + Self::Anyhow(anyhow::anyhow!(value.to_string())) + } +} + +impl Serialize for CommandError { + fn serialize(&self, serializer: S) -> std::result::Result + where + S: Serializer, + { + if let Self::Anyhow(err) = self { + serializer.serialize_str(format!("{err:#}").as_ref()) + } else { + serializer.serialize_str(self.to_string().as_ref()) + } + } +} + +pub type CommandResult = std::result::Result; + +/// Represents either a plain PathBuf or a PathHandle that manages security-scoped resources. +pub enum PathKind { + /// A plain path that doesn't manage security-scoped resources. + #[cfg(mobile)] // only used on mobile + Path(PathBuf), + /// A path handle that manages security-scoped resources and will clean them up on drop. + Handle(PathHandle), +} + +impl PathKind { + /// Get a reference to the underlying path. + pub fn as_path(&self) -> &Path { + match self { + #[cfg(mobile)] + PathKind::Path(p) => p.as_ref(), + PathKind::Handle(h) => h.as_ref(), + } + } + + /// Get a reference to the underlying PathBuf. + pub fn as_path_buf(&self) -> &PathBuf { + match self { + #[cfg(mobile)] + PathKind::Path(p) => p, + PathKind::Handle(h) => h, + } + } +} + +impl AsRef for PathKind { + fn as_ref(&self) -> &Path { + self.as_path() + } +} + +impl AsRef for PathKind { + fn as_ref(&self) -> &PathBuf { + self.as_path_buf() + } +} + +/// A file handle that automatically stops accessing security-scoped resources on iOS when dropped. +pub struct FileHandle { + file: File, + path: PathKind, + #[cfg(target_os = "ios")] + path_: SafeFilePath, + #[cfg(target_os = "ios")] + app_handle: tauri::AppHandle, +} + +impl FileHandle { + /// Get the resolved path. + pub fn path(&self) -> &Path { + self.path.as_path() + } +} + +impl Deref for FileHandle { + type Target = File; + + fn deref(&self) -> &Self::Target { + &self.file + } +} + +impl DerefMut for FileHandle { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.file + } +} + +#[cfg(target_os = "ios")] +impl Drop for FileHandle { + fn drop(&mut self) { + // Only clean up if we have a plain PathBuf, not a PathHandle + // PathHandle will handle its own cleanup when it's dropped + if let PathKind::Path(_) = &self.path { + use crate::{FilePath, FsExt}; + // Convert SafeFilePath to FilePath + let file_path: FilePath = match &self.path_ { + SafeFilePath::Url(url) => FilePath::Url(url.clone()), + SafeFilePath::Path(safe_path) => FilePath::Path(safe_path.as_ref().to_owned()), + }; + + // Only clean up if we're tracking this resource + // If start_accessing_security_scoped_resource was used, it won't be in our tracking + // and we shouldn't interfere + if let FilePath::Url(url) = file_path { + if url.scheme() == "file" { + let security_scoped_resources = + self.app_handle.state::(); + + // Only clean up if it's not tracked manually + if !security_scoped_resources.is_tracked_manually(url.as_str()) { + log::debug!( + "Stopping accessing security-scoped resource for URL: {url} on drop" + ); + let _ = self + .app_handle + .fs() + .stop_accessing_security_scoped_resource(FilePath::Url(url.clone())); + security_scoped_resources.remove(url.as_str()); + } else { + log::debug!("Not cleaning up security-scoped resource for URL: {url} on drop (manually tracked via start_accessing_security_scoped_resource)"); + } + } + } + } + } +} + +/// A path handle that automatically stops accessing security-scoped resources on iOS when dropped. +pub struct PathHandle { + path: PathBuf, + #[allow(dead_code)] // Used in Drop implementation + path_: SafeFilePath, + #[allow(dead_code)] // Used in Drop implementation + app_handle: tauri::AppHandle, +} + +impl PathHandle { + fn new(path: PathBuf, path_: SafeFilePath, app_handle: tauri::AppHandle) -> Self { + Self { + path, + path_, + app_handle, + } + } +} + +impl Deref for PathHandle { + type Target = PathBuf; + + fn deref(&self) -> &Self::Target { + &self.path + } +} + +impl AsRef for PathHandle { + fn as_ref(&self) -> &Path { + self.path.as_ref() + } +} + +impl AsRef for PathHandle { + fn as_ref(&self) -> &PathBuf { + &self.path + } +} + +#[cfg(target_os = "ios")] +impl Drop for PathHandle { + fn drop(&mut self) { + use crate::{FilePath, FsExt}; + // Convert SafeFilePath to FilePath + let file_path: FilePath = match &self.path_ { + SafeFilePath::Url(url) => FilePath::Url(url.clone()), + SafeFilePath::Path(safe_path) => FilePath::Path(safe_path.as_ref().to_owned()), + }; + + // Only clean up if we're tracking this resource (i.e., resolve_path started it) + // If start_accessing_security_scoped_resource was used, it won't be in our tracking + // and we shouldn't interfere + if let FilePath::Url(url) = file_path { + if url.scheme() == "file" { + let security_scoped_resources = + self.app_handle.state::(); + + // Only clean up if it's not tracked manually + if !security_scoped_resources.is_tracked_manually(url.as_str()) { + log::debug!( + "Stopping accessing security-scoped resource for URL: {url} on drop" + ); + let _ = self + .app_handle + .fs() + .stop_accessing_security_scoped_resource(FilePath::Url(url.clone())); + security_scoped_resources.remove(url.as_str()); + } else { + log::debug!("Not cleaning up security-scoped resource for URL: {url} on drop (manually tracked via start_accessing_security_scoped_resource)"); + } + } + } + } +} + +#[derive(Debug, Default, Clone, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct BaseOptions { + base_dir: Option, +} + +#[tauri::command] +pub fn create( + webview: Webview, + global_scope: GlobalScope, + command_scope: CommandScope, + path: SafeFilePath, + options: Option, +) -> CommandResult { + #[cfg(target_os = "ios")] + let path_ = path.clone(); + let resolved_path_handle = resolve_path( + "create", + &webview, + &global_scope, + &command_scope, + path, + options.and_then(|o| o.base_dir), + )?; + let file = File::create(&*resolved_path_handle).map_err(|e| { + format!( + "failed to create file at path: {} with error: {e}", + resolved_path_handle.display() + ) + })?; + #[cfg(target_os = "ios")] + let app_handle = webview.app_handle().clone(); + let file_handle = FileHandle { + file, + path: PathKind::Handle(resolved_path_handle), + #[cfg(target_os = "ios")] + path_, + #[cfg(target_os = "ios")] + app_handle, + }; + let rid = webview + .resources_table() + .add(StdFileResource::new(file_handle)); + Ok(rid) +} + +#[derive(Debug, Default, Clone, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct OpenOptions { + #[serde(flatten)] + base: BaseOptions, + #[serde(flatten)] + options: crate::OpenOptions, +} + +#[tauri::command] +pub fn open( + webview: Webview, + global_scope: GlobalScope, + command_scope: CommandScope, + path: SafeFilePath, + options: Option, +) -> CommandResult { + let file_handle = resolve_file( + "open", + &webview, + &global_scope, + &command_scope, + path, + if let Some(opts) = options { + OpenOptions { + base: opts.base, + options: opts.options, + } + } else { + OpenOptions { + base: BaseOptions { base_dir: None }, + options: crate::OpenOptions { + read: true, + write: false, + truncate: false, + create: false, + create_new: false, + append: false, + mode: None, + custom_flags: None, + }, + } + }, + )?; + + let rid = webview + .resources_table() + .add(StdFileResource::new(file_handle)); + + Ok(rid) +} + +#[derive(Debug, Clone, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct CopyFileOptions { + from_path_base_dir: Option, + to_path_base_dir: Option, +} + +#[tauri::command] +pub async fn copy_file( + webview: Webview, + global_scope: GlobalScope, + command_scope: CommandScope, + from_path: SafeFilePath, + to_path: SafeFilePath, + options: Option, +) -> CommandResult<()> { + let resolved_from_path = resolve_path( + "copy-file", + &webview, + &global_scope, + &command_scope, + from_path, + options.as_ref().and_then(|o| o.from_path_base_dir), + )?; + let resolved_to_path = resolve_path( + "copy-file", + &webview, + &global_scope, + &command_scope, + to_path, + options.as_ref().and_then(|o| o.to_path_base_dir), + )?; + std::fs::copy(&resolved_from_path, &resolved_to_path).map_err(|e| { + format!( + "failed to copy file from path: {}, to path: {} with error: {e}", + resolved_from_path.display(), + resolved_to_path.display() + ) + })?; + Ok(()) +} + +#[derive(Debug, Clone, Deserialize)] +pub struct MkdirOptions { + #[serde(flatten)] + base: BaseOptions, + #[allow(unused)] + mode: Option, + recursive: Option, +} + +#[tauri::command] +pub fn mkdir( + webview: Webview, + global_scope: GlobalScope, + command_scope: CommandScope, + path: SafeFilePath, + options: Option, +) -> CommandResult<()> { + let resolved_path = resolve_path( + "mkdir", + &webview, + &global_scope, + &command_scope, + path, + options.as_ref().and_then(|o| o.base.base_dir), + )?; + + let mut builder = std::fs::DirBuilder::new(); + builder.recursive(options.as_ref().and_then(|o| o.recursive).unwrap_or(false)); + + #[cfg(unix)] + { + use std::os::unix::fs::DirBuilderExt; + let mode = options.as_ref().and_then(|o| o.mode).unwrap_or(0o777) & 0o777; + builder.mode(mode); + } + + builder + .create(&resolved_path) + .map_err(|e| { + format!( + "failed to create directory at path: {} with error: {e}", + resolved_path.display() + ) + }) + .map_err(Into::into) +} + +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +#[non_exhaustive] +pub struct DirEntry { + pub name: String, + pub is_directory: bool, + pub is_file: bool, + pub is_symlink: bool, +} + +#[tauri::command] +pub async fn read_dir( + webview: Webview, + global_scope: GlobalScope, + command_scope: CommandScope, + path: SafeFilePath, + options: Option, +) -> CommandResult> { + let resolved_path = resolve_path( + "read-dir", + &webview, + &global_scope, + &command_scope, + path, + options.as_ref().and_then(|o| o.base_dir), + )?; + + let entries = std::fs::read_dir(&resolved_path).map_err(|e| { + format!( + "failed to read directory at path: {} with error: {e}", + resolved_path.display() + ) + })?; + + let entries = entries + .filter_map(|entry| { + let entry = entry.ok()?; + let name = entry.file_name().into_string().ok()?; + let metadata = entry.file_type(); + macro_rules! method_or_false { + ($method:ident) => { + if let Ok(metadata) = &metadata { + metadata.$method() + } else { + false + } + }; + } + Some(DirEntry { + name, + is_file: method_or_false!(is_file), + is_directory: method_or_false!(is_dir), + is_symlink: method_or_false!(is_symlink), + }) + }) + .collect(); + + Ok(entries) +} + +#[tauri::command] +pub async fn read( + webview: Webview, + rid: ResourceId, + len: usize, +) -> CommandResult { + let mut data = vec![0; len]; + let file: std::sync::Arc> = webview.resources_table().get(rid)?; + let nread = StdFileResource::with_lock(&file, |file| file.read(&mut data)) + .map_err(|e| format!("faied to read bytes from file with error: {e}"))?; + + // This is an optimization to include the number of read bytes (as bigendian bytes) + // at the end of returned vector so we can use `tauri::ipc::Response` + // and avoid serialization overhead of separate values. + #[cfg(target_pointer_width = "16")] + let nread = { + let nread = nread.to_be_bytes(); + let mut out = [0; 8]; + out[6..].copy_from_slice(&nread); + out + }; + #[cfg(target_pointer_width = "32")] + let nread = { + let nread = nread.to_be_bytes(); + let mut out = [0; 8]; + out[4..].copy_from_slice(&nread); + out + }; + #[cfg(target_pointer_width = "64")] + let nread = nread.to_be_bytes(); + + data.extend(nread); + + Ok(tauri::ipc::Response::new(data)) +} + +async fn read_file_inner( + permission: &str, + webview: Webview, + global_scope: GlobalScope, + command_scope: CommandScope, + path: SafeFilePath, + options: Option, +) -> CommandResult { + let mut file_handle = resolve_file( + permission, + &webview, + &global_scope, + &command_scope, + path, + OpenOptions { + base: BaseOptions { + base_dir: options.as_ref().and_then(|o| o.base_dir), + }, + options: crate::OpenOptions { + read: true, + ..Default::default() + }, + }, + )?; + + let mut contents = Vec::new(); + + file_handle.read_to_end(&mut contents).map_err(|e| { + format!( + "failed to read file as text at path: {} with error: {e}", + file_handle.path().display() + ) + })?; + + Ok(tauri::ipc::Response::new(contents)) +} + +#[tauri::command] +pub async fn read_file( + webview: Webview, + global_scope: GlobalScope, + command_scope: CommandScope, + path: SafeFilePath, + options: Option, +) -> CommandResult { + read_file_inner( + "read-file", + webview, + global_scope, + command_scope, + path, + options, + ) + .await +} + +#[derive(Debug, Default, Clone, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ReadTextFileOptions { + #[serde(flatten)] + base: BaseOptions, + encoding: Option, +} + +// TODO, remove in v3, rely on `read_file` command instead +#[tauri::command] +pub async fn read_text_file( + webview: Webview, + global_scope: GlobalScope, + command_scope: CommandScope, + path: SafeFilePath, + options: Option, +) -> CommandResult { + read_file_inner( + "read-text-file", + webview, + global_scope, + command_scope, + path, + options, + ) + .await +} + +#[tauri::command] +pub fn read_text_file_lines( + webview: Webview, + global_scope: GlobalScope, + command_scope: CommandScope, + path: SafeFilePath, + options: Option, +) -> CommandResult { + let resolved_path = resolve_path( + "read-text-file-lines", + &webview, + &global_scope, + &command_scope, + path, + options.as_ref().and_then(|o| o.base.base_dir), + )?; + + let file = File::open(&resolved_path).map_err(|e| { + format!( + "failed to open file at path: {} with error: {e}", + resolved_path.display() + ) + })?; + + let encoding = options.as_ref().and_then(|o| o.encoding.as_deref()); + let (lf_bytes, cr_bytes) = lf_cr_bytes_for_encoding_label(encoding); + let lines = BufReader::new(file); + let rid = webview + .resources_table() + .add(StdLinesResource::new(lines, lf_bytes, cr_bytes)); + + Ok(rid) +} + +/// Returns the byte sequences for LF (`\n`) and CR (`\r`) in the encoding label. +/// +/// The provided encoding label must be a normalized, lowercase string, +/// such as one obtained via `(new TextDecoder(encoding)).encoding`. +/// +/// +fn lf_cr_bytes_for_encoding_label(label: Option<&str>) -> (Vec, Vec) { + // Defaults to utf-8 + // https://developer.mozilla.org/ja/docs/Web/API/TextDecoder/TextDecoder#label + let label = label.unwrap_or("utf-8"); + + // Currently, according to the Web Standard, + // the ASCII-incompatible encodings are UTF-16LE/BE and ISO-2022-JP. + // However, ISO-2022-JP can still detect line breaks in the same way as ASCII. + // + // https://encoding.spec.whatwg.org/#security-background + if label == "utf-16le" { + return (vec![0x0A, 0x00], vec![0x0D, 0x00]); + } + if label == "utf-16be" { + return (vec![0x00, 0x0A], vec![0x00, 0x0D]); + } + + // ASCII-compatible + (vec![b'\n'], vec![b'\r']) +} + +#[tauri::command] +pub async fn read_text_file_lines_next( + webview: Webview, + rid: ResourceId, +) -> CommandResult { + let mut resource_table = webview.resources_table(); + let lines = resource_table.get::(rid)?; + + let ret = StdLinesResource::with_lock(&lines, |lines| -> CommandResult> { + // This is an optimization to include whether we finished iteration or not (1 or 0) + // at the end of returned vector so we can use `tauri::ipc::Response` + // and avoid serialization overhead of separate values. + match lines.next() { + Some(Ok(mut bytes)) => { + bytes.push(false as u8); + Ok(bytes) + } + Some(Err(_)) => Ok(vec![false as u8]), + None => { + resource_table.close(rid)?; + Ok(vec![true as u8]) + } + } + }); + + ret.map(tauri::ipc::Response::new) +} + +#[derive(Debug, Clone, Deserialize)] +pub struct RemoveOptions { + #[serde(flatten)] + base: BaseOptions, + recursive: Option, +} + +#[tauri::command] +pub fn remove( + webview: Webview, + global_scope: GlobalScope, + command_scope: CommandScope, + path: SafeFilePath, + options: Option, +) -> CommandResult<()> { + let resolved_path = resolve_path( + "remove", + &webview, + &global_scope, + &command_scope, + path, + options.as_ref().and_then(|o| o.base.base_dir), + )?; + + let metadata = std::fs::symlink_metadata(&resolved_path).map_err(|e| { + format!( + "failed to get metadata of path: {} with error: {e}", + resolved_path.display() + ) + })?; + + let file_type = metadata.file_type(); + + // taken from deno source code: https://github.com/denoland/deno/blob/429759fe8b4207240709c240a8344d12a1e39566/runtime/ops/fs.rs#L728 + let res = if file_type.is_file() { + std::fs::remove_file(&resolved_path) + } else if options.as_ref().and_then(|o| o.recursive).unwrap_or(false) { + std::fs::remove_dir_all(&resolved_path) + } else if file_type.is_symlink() { + #[cfg(unix)] + { + std::fs::remove_file(&resolved_path) + } + #[cfg(not(unix))] + { + use std::os::windows::fs::MetadataExt; + const FILE_ATTRIBUTE_DIRECTORY: u32 = 0x00000010; + if metadata.file_attributes() & FILE_ATTRIBUTE_DIRECTORY != 0 { + std::fs::remove_dir(&resolved_path) + } else { + std::fs::remove_file(&resolved_path) + } + } + } else if file_type.is_dir() { + std::fs::remove_dir(&resolved_path) + } else { + // pipes, sockets, etc... + std::fs::remove_file(&resolved_path) + }; + + res.map_err(|e| { + format!( + "failed to remove path: {} with error: {e}", + resolved_path.display() + ) + }) + .map_err(Into::into) +} + +#[derive(Debug, Clone, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct RenameOptions { + new_path_base_dir: Option, + old_path_base_dir: Option, +} + +#[tauri::command] +pub fn rename( + webview: Webview, + global_scope: GlobalScope, + command_scope: CommandScope, + old_path: SafeFilePath, + new_path: SafeFilePath, + options: Option, +) -> CommandResult<()> { + let resolved_old_path = resolve_path( + "rename", + &webview, + &global_scope, + &command_scope, + old_path, + options.as_ref().and_then(|o| o.old_path_base_dir), + )?; + let resolved_new_path = resolve_path( + "rename", + &webview, + &global_scope, + &command_scope, + new_path, + options.as_ref().and_then(|o| o.new_path_base_dir), + )?; + std::fs::rename(&resolved_old_path, &resolved_new_path) + .map_err(|e| { + format!( + "failed to rename old path: {} to new path: {} with error: {e}", + resolved_old_path.display(), + resolved_new_path.display() + ) + }) + .map_err(Into::into) +} + +#[derive(Serialize_repr, Deserialize_repr, Clone, Copy, Debug)] +#[repr(u16)] +pub enum SeekMode { + Start = 0, + Current = 1, + End = 2, +} + +#[tauri::command] +pub async fn seek( + webview: Webview, + rid: ResourceId, + offset: i64, + whence: SeekMode, +) -> CommandResult { + use std::io::{Seek, SeekFrom}; + let file: std::sync::Arc> = webview.resources_table().get(rid)?; + StdFileResource::with_lock(&file, |file| { + file.seek(match whence { + SeekMode::Start => SeekFrom::Start(offset as u64), + SeekMode::Current => SeekFrom::Current(offset), + SeekMode::End => SeekFrom::End(offset), + }) + }) + .map_err(|e| format!("failed to seek file with error: {e}")) + .map_err(Into::into) +} + +#[cfg(target_os = "android")] +fn get_metadata std::io::Result>( + permission: &str, + metadata_fn: F, + webview: &Webview, + global_scope: &GlobalScope, + command_scope: &CommandScope, + path: SafeFilePath, + options: Option, +) -> CommandResult { + match path { + SafeFilePath::Url(url) => { + let file_handle = resolve_file( + permission, + webview, + global_scope, + command_scope, + SafeFilePath::Url(url), + OpenOptions { + base: BaseOptions { base_dir: None }, + options: crate::OpenOptions { + read: true, + ..Default::default() + }, + }, + )?; + file_handle.metadata().map_err(|e| { + format!( + "failed to get metadata of path: {} with error: {e}", + file_handle.path().display() + ) + .into() + }) + } + SafeFilePath::Path(p) => get_fs_metadata( + permission, + metadata_fn, + webview, + global_scope, + command_scope, + SafeFilePath::Path(p), + options, + ), + } +} + +#[cfg(not(target_os = "android"))] +fn get_metadata std::io::Result>( + permission: &str, + metadata_fn: F, + webview: &Webview, + global_scope: &GlobalScope, + command_scope: &CommandScope, + path: SafeFilePath, + options: Option, +) -> CommandResult { + get_fs_metadata( + permission, + metadata_fn, + webview, + global_scope, + command_scope, + path, + options, + ) +} + +fn get_fs_metadata std::io::Result>( + permission: &str, + metadata_fn: F, + webview: &Webview, + global_scope: &GlobalScope, + command_scope: &CommandScope, + path: SafeFilePath, + options: Option, +) -> CommandResult { + let resolved_path = resolve_path( + permission, + webview, + global_scope, + command_scope, + path, + options.as_ref().and_then(|o| o.base_dir), + )?; + let metadata = metadata_fn(&resolved_path).map_err(|e| { + format!( + "failed to get metadata of path: {} with error: {e}", + resolved_path.display() + ) + })?; + Ok(metadata) +} + +#[tauri::command] +pub fn stat( + webview: Webview, + global_scope: GlobalScope, + command_scope: CommandScope, + path: SafeFilePath, + options: Option, +) -> CommandResult { + let metadata = get_metadata( + "stat", + |p| std::fs::metadata(p), + &webview, + &global_scope, + &command_scope, + path, + options, + )?; + + Ok(get_stat(metadata)) +} + +#[tauri::command] +pub fn lstat( + webview: Webview, + global_scope: GlobalScope, + command_scope: CommandScope, + path: SafeFilePath, + options: Option, +) -> CommandResult { + let metadata = get_metadata( + "lstat", + |p| std::fs::symlink_metadata(p), + &webview, + &global_scope, + &command_scope, + path, + options, + )?; + Ok(get_stat(metadata)) +} + +#[tauri::command] +pub fn fstat(webview: Webview, rid: ResourceId) -> CommandResult { + let file: std::sync::Arc> = webview.resources_table().get(rid)?; + let metadata = StdFileResource::with_lock(&file, |file| file.metadata()) + .map_err(|e| format!("failed to get metadata of file with error: {e}"))?; + Ok(get_stat(metadata)) +} + +#[tauri::command] +pub async fn truncate( + webview: Webview, + global_scope: GlobalScope, + command_scope: CommandScope, + path: SafeFilePath, + len: Option, + options: Option, +) -> CommandResult<()> { + let resolved_path = resolve_path( + "truncate", + &webview, + &global_scope, + &command_scope, + path, + options.as_ref().and_then(|o| o.base_dir), + )?; + let f = std::fs::OpenOptions::new() + .write(true) + .open(&resolved_path) + .map_err(|e| { + format!( + "failed to open file at path: {} with error: {e}", + resolved_path.display() + ) + })?; + f.set_len(len.unwrap_or(0)) + .map_err(|e| { + format!( + "failed to truncate file at path: {} with error: {e}", + resolved_path.display() + ) + }) + .map_err(Into::into) +} + +#[tauri::command] +pub async fn ftruncate( + webview: Webview, + rid: ResourceId, + len: Option, +) -> CommandResult<()> { + let file: std::sync::Arc> = webview.resources_table().get(rid)?; + StdFileResource::with_lock(&file, |file| file.set_len(len.unwrap_or(0))) + .map_err(|e| format!("failed to truncate file with error: {e}")) + .map_err(Into::into) +} + +#[tauri::command] +pub async fn write( + webview: Webview, + rid: ResourceId, + data: Vec, +) -> CommandResult { + let file: std::sync::Arc> = webview.resources_table().get(rid)?; + StdFileResource::with_lock(&file, |file| file.write(&data)) + .map_err(|e| format!("failed to write bytes to file with error: {e}")) + .map_err(Into::into) +} + +#[derive(Debug, Clone, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct WriteFileOptions { + #[serde(flatten)] + base: BaseOptions, + #[serde(default)] + append: bool, + #[serde(default = "default_create_value")] + create: bool, + #[serde(default)] + create_new: bool, + #[allow(unused)] + mode: Option, +} + +fn default_create_value() -> bool { + true +} + +async fn write_file_inner( + permission: &str, + webview: Webview, + global_scope: GlobalScope, + command_scope: CommandScope, + request: tauri::ipc::Request<'_>, +) -> CommandResult<()> { + let path = request + .headers() + .get("path") + .ok_or_else(|| anyhow::anyhow!("missing file path").into()) + .and_then(|p| { + percent_encoding::percent_decode(p.as_ref()) + .decode_utf8() + .map_err(|_| anyhow::anyhow!("path is not a valid UTF-8").into()) + }) + .and_then(|p| SafeFilePath::from_str(&p).map_err(CommandError::from))?; + + let options: Option = request + .headers() + .get("options") + .and_then(|p| p.to_str().ok()) + .and_then(|opts| serde_json::from_str(opts).ok()); + + let mut file_handle = resolve_file( + permission, + &webview, + &global_scope, + &command_scope, + path, + if let Some(opts) = options { + OpenOptions { + base: opts.base, + options: crate::OpenOptions { + read: false, + write: true, + create: opts.create, + truncate: !opts.append, + append: opts.append, + create_new: opts.create_new, + mode: opts.mode, + custom_flags: None, + }, + } + } else { + OpenOptions { + base: BaseOptions { base_dir: None }, + options: crate::OpenOptions { + read: false, + write: true, + truncate: true, + create: true, + create_new: false, + append: false, + mode: None, + custom_flags: None, + }, + } + }, + )?; + + let data = match request.body() { + tauri::ipc::InvokeBody::Raw(data) => Cow::Borrowed(data), + tauri::ipc::InvokeBody::Json(serde_json::Value::Array(data)) => Cow::Owned( + data.iter() + .flat_map(|v| v.as_number().and_then(|v| v.as_u64().map(|v| v as u8))) + .collect(), + ), + _ => return Err(anyhow::anyhow!("unexpected invoke body").into()), + }; + + file_handle + .write_all(&data) + .map_err(|e| { + format!( + "failed to write bytes to file at path: {} with error: {e}", + file_handle.path().display() + ) + }) + .map_err(Into::into) +} + +#[tauri::command] +pub async fn write_file( + webview: Webview, + global_scope: GlobalScope, + command_scope: CommandScope, + request: tauri::ipc::Request<'_>, +) -> CommandResult<()> { + write_file_inner("write-file", webview, global_scope, command_scope, request).await +} + +// TODO, remove in v3, rely on `write_file` command instead +#[tauri::command] +pub async fn write_text_file( + webview: Webview, + global_scope: GlobalScope, + command_scope: CommandScope, + request: tauri::ipc::Request<'_>, +) -> CommandResult<()> { + write_file_inner( + "write-text-file", + webview, + global_scope, + command_scope, + request, + ) + .await +} + +#[tauri::command] +pub fn exists( + webview: Webview, + global_scope: GlobalScope, + command_scope: CommandScope, + path: SafeFilePath, + options: Option, +) -> CommandResult { + let resolved_path = resolve_path( + "exists", + &webview, + &global_scope, + &command_scope, + path, + options.as_ref().and_then(|o| o.base_dir), + )?; + Ok(resolved_path.exists()) +} + +#[tauri::command] +pub async fn size( + webview: Webview, + global_scope: GlobalScope, + command_scope: CommandScope, + path: SafeFilePath, + options: Option, +) -> CommandResult { + let resolved_path = resolve_path( + "size", + &webview, + &global_scope, + &command_scope, + path, + options.as_ref().and_then(|o| o.base_dir), + )?; + + let metadata = resolved_path.metadata()?; + + if metadata.is_file() { + Ok(metadata.len()) + } else { + let size = get_dir_size(&resolved_path).map_err(|e| { + format!( + "failed to get size at path: {} with error: {e}", + resolved_path.display() + ) + })?; + + Ok(size) + } +} + +#[tauri::command] +pub fn start_accessing_security_scoped_resource( + webview: Webview, + path: SafeFilePath, +) -> CommandResult<()> { + #[cfg(target_os = "ios")] + { + use crate::FilePath; + // Convert SafeFilePath to FilePath + let file_path: FilePath = match &path { + SafeFilePath::Url(url) => FilePath::Url(url.clone()), + SafeFilePath::Path(safe_path) => FilePath::Path(safe_path.as_ref().to_owned()), + }; + + // Only handle file URLs + if let FilePath::Url(url) = &file_path { + if url.scheme() == "file" { + use objc2_foundation::{NSString, NSURL}; + + let url_nsstring = NSString::from_str(url.as_str()); + let ns_url = unsafe { NSURL::URLWithString(&url_nsstring) }; + if let Some(ns_url) = ns_url { + // Check if already active + let security_scoped_resources = + webview.state::(); + if security_scoped_resources.is_tracked_manually(url.as_str()) { + log::debug!( + "Security-scoped resource already active for URL: {}", + url.as_str() + ); + return Ok(()); + } + + // Start accessing the security-scoped resource + unsafe { + let success = ns_url.startAccessingSecurityScopedResource(); + if success { + log::debug!( + "Started accessing security-scoped resource for URL: {}", + url.as_str() + ); + security_scoped_resources.track_manually(url.as_str().to_string()); + } else { + log::warn!( + "Failed to start accessing security-scoped resource for URL: {}", + url.as_str() + ); + return Err(CommandError::from(format!( + "Failed to start accessing security-scoped resource for URL: {}", + url.as_str() + ))); + } + } + } else { + return Err(CommandError::from(format!( + "Failed to create NSURL from URL: {}", + url.as_str() + ))); + } + } + } + Ok(()) + } + #[cfg(not(target_os = "ios"))] + { + // No-op on non-iOS platforms + let _ = webview; + let _ = path; + Ok(()) + } +} + +#[tauri::command] +pub fn stop_accessing_security_scoped_resource( + webview: Webview, + path: SafeFilePath, +) -> CommandResult<()> { + #[cfg(target_os = "ios")] + { + use crate::{FilePath, FsExt}; + // Convert SafeFilePath to FilePath + let file_path: FilePath = match &path { + SafeFilePath::Url(url) => FilePath::Url(url.clone()), + SafeFilePath::Path(safe_path) => FilePath::Path(safe_path.as_ref().to_owned()), + }; + + // Only handle file URLs + if let FilePath::Url(url) = file_path { + if url.scheme() == "file" { + let security_scoped_resources = webview.state::(); + + // Check if it's tracked + if !security_scoped_resources.is_tracked_manually(url.as_str()) { + log::debug!( + "Security-scoped resource not tracked as active for URL: {}", + url.as_str() + ); + return Ok(()); + } + + // Stop accessing the security-scoped resource + webview + .fs() + .stop_accessing_security_scoped_resource(FilePath::Url(url.clone()))?; + + // Remove from tracking + security_scoped_resources.remove(url.as_str()); + log::debug!( + "Stopped accessing security-scoped resource for URL: {}", + url.as_str() + ); + } + } + Ok(()) + } + #[cfg(not(target_os = "ios"))] + { + // No-op on non-iOS platforms + let _ = webview; + let _ = path; + Ok(()) + } +} + +fn get_dir_size(path: &PathBuf) -> CommandResult { + let mut size = 0; + + for entry in std::fs::read_dir(path)? { + let entry = entry?; + let metadata = entry.metadata()?; + + if metadata.is_file() { + size += metadata.len(); + } else if metadata.is_dir() { + size += get_dir_size(&entry.path())?; + } + } + + Ok(size) +} + +#[cfg(desktop)] +pub fn resolve_file( + permission: &str, + webview: &Webview, + global_scope: &GlobalScope, + command_scope: &CommandScope, + path: SafeFilePath, + open_options: OpenOptions, +) -> CommandResult> { + resolve_file_in_fs( + permission, + webview, + global_scope, + command_scope, + path, + open_options, + ) +} + +fn resolve_file_in_fs( + permission: &str, + webview: &Webview, + global_scope: &GlobalScope, + command_scope: &CommandScope, + path: SafeFilePath, + open_options: OpenOptions, +) -> CommandResult> { + #[cfg(target_os = "ios")] + let path_ = path.clone(); + let resolved_path_handle = resolve_path( + permission, + webview, + global_scope, + command_scope, + path, + open_options.base.base_dir, + )?; + + let file = std::fs::OpenOptions::from(open_options.options) + .open(&*resolved_path_handle) + .map_err(|e| { + format!( + "failed to open file at path: {} with error: {e}", + resolved_path_handle.display() + ) + })?; + + #[cfg(target_os = "ios")] + let app_handle = webview.app_handle().clone(); + Ok(FileHandle { + file, + path: PathKind::Handle(resolved_path_handle), + #[cfg(target_os = "ios")] + path_, + #[cfg(target_os = "ios")] + app_handle, + }) +} + +#[cfg(mobile)] +pub fn resolve_file( + permission: &str, + webview: &Webview, + global_scope: &GlobalScope, + command_scope: &CommandScope, + path: SafeFilePath, + open_options: OpenOptions, +) -> CommandResult> { + use crate::FsExt; + + #[cfg(target_os = "ios")] + let path_ = path.clone(); + match path { + SafeFilePath::Url(url) => { + let resolved_path = url.as_str().into(); + let file = webview + .fs() + .open(SafeFilePath::Url(url.clone()), open_options.options)?; + #[cfg(target_os = "ios")] + let app_handle = webview.app_handle().clone(); + Ok(FileHandle { + file, + path: PathKind::Path(resolved_path), + #[cfg(target_os = "ios")] + path_, + #[cfg(target_os = "ios")] + app_handle, + }) + } + SafeFilePath::Path(path) => resolve_file_in_fs( + permission, + webview, + global_scope, + command_scope, + SafeFilePath::Path(path), + open_options, + ), + } +} + +pub fn resolve_path( + permission: &str, + webview: &Webview, + global_scope: &GlobalScope, + command_scope: &CommandScope, + path: SafeFilePath, + base_dir: Option, +) -> CommandResult> { + let path_ = path.clone(); + // On iOS, start accessing security-scoped resource if the path is a file URL + // Only if it hasn't been started already via start_accessing_security_scoped_resource + #[cfg(target_os = "ios")] + if let SafeFilePath::Url(url) = &path { + if url.scheme() == "file" { + use objc2_foundation::{NSString, NSURL}; + + let security_scoped_resources = webview.state::(); + + // Check if already active (started via start_accessing_security_scoped_resource) + if !security_scoped_resources.is_tracked_manually(url.as_str()) { + let url_nsstring = NSString::from_str(url.as_str()); + let ns_url = unsafe { NSURL::URLWithString(&url_nsstring) }; + if let Some(ns_url) = ns_url { + // Start accessing the security-scoped resource + // This is required for files outside the app's sandbox (e.g., from file picker) + unsafe { + let success = ns_url.startAccessingSecurityScopedResource(); + if success { + log::debug!("Started accessing security-scoped resource for URL: {} (via resolve_path)", url.as_str()); + // Track it so we know to clean it up + security_scoped_resources.track_manually(url.as_str().to_string()); + } else { + log::warn!( + "Failed to start accessing security-scoped resource for URL: {}", + url.as_str() + ); + } + } + } else { + log::debug!("Failed to create NSURL from URL: {}, ignoring security-scoped resource access request", url.as_str()); + } + } else { + log::debug!("Security-scoped resource already active for URL: {} (started via start_accessing_security_scoped_resource), skipping", url.as_str()); + } + } + } + + let path = path.into_path()?; + let resolved_path = if let Some(base_dir) = base_dir { + webview.path().resolve(&path, base_dir)? + } else { + path + }; + + let fs_scope = webview.state::(); + + let scope = tauri::scope::fs::Scope::new( + webview, + &FsScope::Scope { + allow: global_scope + .allows() + .iter() + .filter_map(|e| e.path.clone()) + .chain(command_scope.allows().iter().filter_map(|e| e.path.clone())) + .collect(), + deny: global_scope + .denies() + .iter() + .filter_map(|e| e.path.clone()) + .chain(command_scope.denies().iter().filter_map(|e| e.path.clone())) + .collect(), + require_literal_leading_dot: fs_scope.require_literal_leading_dot, + }, + )?; + + let require_literal_leading_dot = fs_scope.require_literal_leading_dot.unwrap_or(cfg!(unix)); + + if is_forbidden(&fs_scope.scope, &resolved_path, require_literal_leading_dot) + || is_forbidden(&scope, &resolved_path, require_literal_leading_dot) + { + return Err(CommandError::Plugin(Error::PathForbidden(resolved_path))); + } + + if fs_scope.scope.is_allowed(&resolved_path) || scope.is_allowed(&resolved_path) { + let app_handle = webview.app_handle().clone(); + Ok(PathHandle::new(resolved_path, path_, app_handle)) + } else { + #[cfg(not(debug_assertions))] + return Err(CommandError::Plugin(Error::PathForbidden(resolved_path))); + + #[cfg(debug_assertions)] + Err( + anyhow::anyhow!( + "forbidden path: {}, maybe it is not allowed on the scope for `allow-{permission}` permission in your capability file", + resolved_path.display() + ) + ) + .map_err(Into::into) + } +} + +fn is_forbidden>( + scope: &tauri::fs::Scope, + path: P, + require_literal_leading_dot: bool, +) -> bool { + let path = path.as_ref(); + let path = if path.is_symlink() { + match std::fs::read_link(path) { + Ok(p) => p, + Err(_) => return false, + } + } else { + path.to_path_buf() + }; + let path = if !path.exists() { + crate::Result::Ok(path) + } else { + std::fs::canonicalize(path).map_err(Into::into) + }; + + if let Ok(path) = path { + let path: PathBuf = path.components().collect(); + scope.forbidden_patterns().iter().any(|p| { + p.matches_path_with( + &path, + glob::MatchOptions { + // this is needed so `/dir/*` doesn't match files within subdirectories such as `/dir/subdir/file.txt` + // see: + require_literal_separator: true, + require_literal_leading_dot, + ..Default::default() + }, + ) + }) + } else { + false + } +} + +struct StdFileResource(Mutex>); + +impl StdFileResource { + fn new(file_handle: FileHandle) -> Self { + Self(Mutex::new(file_handle)) + } + + fn with_lock Ret>(&self, mut f: F) -> Ret { + let mut file_handle = self.0.lock().unwrap(); + f(&mut file_handle) + } +} + +impl Resource for StdFileResource {} + +/// Same as [std::io::Lines] but with bytes +struct LinesBytes { + bytes: T, + lf_bytes: Vec, + cr_bytes: Vec, +} + +impl LinesBytes { + fn new(bytes: T, lf_bytes: Vec, cr_bytes: Vec) -> Self { + LinesBytes { + bytes, + lf_bytes, + cr_bytes, + } + } +} + +impl Iterator for LinesBytes { + type Item = std::io::Result>; + + fn next(&mut self) -> Option>> { + let mut buf = Vec::new(); + // Search for '\n' + match read_until_bytes(&mut self.bytes, &self.lf_bytes, &mut buf) { + Ok(0) => None, + Ok(_n) => { + // Remove '\n' or '\r\n' + if buf.ends_with(&self.lf_bytes) { + buf.truncate(buf.len() - self.lf_bytes.len()); + if buf.ends_with(&self.cr_bytes) { + buf.truncate(buf.len() - self.cr_bytes.len()); + } + } + + Some(Ok(buf)) + } + Err(e) => Some(Err(e)), + } + } +} + +fn read_until_bytes( + r: &mut impl BufRead, + bytes: &[u8], + buf: &mut Vec, +) -> std::io::Result { + let last_byte = *bytes + .last() + .ok_or_else(|| std::io::Error::other("invalid empty bytes"))?; + + if bytes.len() == 1 { + return r.read_until(last_byte, buf); + } + + let mut total_n = 0; + loop { + let n = r.read_until(last_byte, buf)?; + total_n += n; + + if n == 0 || buf.ends_with(bytes) { + return Ok(total_n); + } + } +} + +struct StdLinesResource(Mutex>>); + +impl StdLinesResource { + fn new(lines: BufReader, lf_bytes: Vec, cr_bytes: Vec) -> Self { + Self(Mutex::new(LinesBytes::new(lines, lf_bytes, cr_bytes))) + } + + fn with_lock>) -> R>(&self, mut f: F) -> R { + let mut lines = self.0.lock().unwrap(); + f(&mut lines) + } +} + +impl Resource for StdLinesResource {} + +// taken from deno source code: https://github.com/denoland/deno/blob/ffffa2f7c44bd26aec5ae1957e0534487d099f48/runtime/ops/fs.rs#L913 +#[inline] +fn to_msec(maybe_time: std::result::Result) -> Option { + match maybe_time { + Ok(time) => { + let msec = time + .duration_since(UNIX_EPOCH) + .map(|t| t.as_millis() as u64) + .unwrap_or_else(|err| err.duration().as_millis() as u64); + Some(msec) + } + Err(_) => None, + } +} + +// taken from deno source code: https://github.com/denoland/deno/blob/ffffa2f7c44bd26aec5ae1957e0534487d099f48/runtime/ops/fs.rs#L926 +#[derive(Debug, Clone, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct FileInfo { + is_file: bool, + is_directory: bool, + is_symlink: bool, + size: u64, + // In milliseconds, like JavaScript. Available on both Unix or Windows. + mtime: Option, + atime: Option, + birthtime: Option, + readonly: bool, + // Following are only valid under Windows. + file_attribues: Option, + // Following are only valid under Unix. + dev: Option, + ino: Option, + mode: Option, + nlink: Option, + uid: Option, + gid: Option, + rdev: Option, + blksize: Option, + blocks: Option, +} + +// taken from deno source code: https://github.com/denoland/deno/blob/ffffa2f7c44bd26aec5ae1957e0534487d099f48/runtime/ops/fs.rs#L950 +#[inline(always)] +fn get_stat(metadata: std::fs::Metadata) -> FileInfo { + // Unix stat member (number types only). 0 if not on unix. + macro_rules! usm { + ($member:ident) => {{ + #[cfg(unix)] + { + Some(metadata.$member()) + } + #[cfg(not(unix))] + { + None + } + }}; + } + + #[cfg(unix)] + use std::os::unix::fs::MetadataExt; + #[cfg(windows)] + use std::os::windows::fs::MetadataExt; + FileInfo { + is_file: metadata.is_file(), + is_directory: metadata.is_dir(), + is_symlink: metadata.file_type().is_symlink(), + size: metadata.len(), + // In milliseconds, like JavaScript. Available on both Unix or Windows. + mtime: to_msec(metadata.modified()), + atime: to_msec(metadata.accessed()), + birthtime: to_msec(metadata.created()), + readonly: metadata.permissions().readonly(), + // Following are only valid under Windows. + #[cfg(windows)] + file_attribues: Some(metadata.file_attributes()), + #[cfg(not(windows))] + file_attribues: None, + // Following are only valid under Unix. + dev: usm!(dev), + ino: usm!(ino), + mode: usm!(mode), + nlink: usm!(nlink), + uid: usm!(uid), + gid: usm!(gid), + rdev: usm!(rdev), + blksize: usm!(blksize), + blocks: usm!(blocks), + } +} + +#[cfg(test)] +mod test { + use std::io::{BufRead, BufReader}; + + use super::LinesBytes; + + #[test] + fn safe_file_path_parse() { + use super::SafeFilePath; + + assert!(matches!( + serde_json::from_str::("\"C:/Users\""), + Ok(SafeFilePath::Path(_)) + )); + assert!(matches!( + serde_json::from_str::("\"file:///C:/Users\""), + Ok(SafeFilePath::Url(_)) + )); + } + + #[test] + fn test_lines_bytes() { + // UTF-8 + { + let base = String::from("line 1\nline2\nline 3\r\nline 4"); + let bytes = base.as_bytes(); + + let string1 = base.lines().collect::(); + let string2 = BufReader::new(bytes) + .lines() + .map_while(Result::ok) + .collect::(); + let string3 = LinesBytes::new(BufReader::new(bytes), vec![b'\n'], vec![b'\r']) + .flatten() + .flat_map(String::from_utf8) + .collect::(); + + assert_eq!(string1, string2); + assert_eq!(string1, string3); + assert_eq!(string2, string3); + } + + // UTF-16 LE + { + fn utf16(text: &str) -> Vec { + text.encode_utf16().flat_map(|u| u.to_le_bytes()).collect() + } + + let base = String::from("line 1\nline2\nline 3\r\nline 4\n"); + let bytes = utf16(&base); + + let mut lines = LinesBytes::new(BufReader::new(&bytes[..]), utf16("\n"), utf16("\r")); + assert_eq!(lines.next().map(Result::unwrap), Some(utf16("line 1"))); + assert_eq!(lines.next().map(Result::unwrap), Some(utf16("line2"))); + assert_eq!(lines.next().map(Result::unwrap), Some(utf16("line 3"))); + assert_eq!(lines.next().map(Result::unwrap), Some(utf16("line 4"))); + assert!(lines.next().is_none()); + } + + // UTF-16 BE + { + fn utf16(text: &str) -> Vec { + text.encode_utf16().flat_map(|u| u.to_be_bytes()).collect() + } + + // ਗ (U+0A17) encodes to 0x0A 0x17, + // which contains 0x0A but is not a line feed (U+000A = 0x00 0x0A). + let base = String::from("line 1\nline2ਗ\nline 3\r\nline 4"); + let bytes = utf16(&base); + + let mut lines = LinesBytes::new(BufReader::new(&bytes[..]), utf16("\n"), utf16("\r")); + assert_eq!(lines.next().map(Result::unwrap), Some(utf16("line 1"))); + assert_eq!(lines.next().map(Result::unwrap), Some(utf16("line2ਗ"))); + assert_eq!(lines.next().map(Result::unwrap), Some(utf16("line 3"))); + assert_eq!(lines.next().map(Result::unwrap), Some(utf16("line 4"))); + assert!(lines.next().is_none()); + } + } +} diff --git a/plugins/fs/src/config.rs b/plugins/fs/src/config.rs new file mode 100644 index 0000000..db3bae4 --- /dev/null +++ b/plugins/fs/src/config.rs @@ -0,0 +1,19 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use serde::Deserialize; + +#[derive(Deserialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct Config { + /// Whether or not paths that contain components that start with a `.` + /// will require that `.` appears literally in the pattern; `*`, `?`, `**`, + /// or `[...]` will not match. This is useful because such files are + /// conventionally considered hidden on Unix systems and it might be + /// desirable to skip them when listing files. + /// + /// Defaults to `true` on Unix systems and `false` on Windows + // dotfiles are not supposed to be exposed by default on unix + pub require_literal_leading_dot: Option, +} diff --git a/plugins/fs/src/desktop.rs b/plugins/fs/src/desktop.rs new file mode 100644 index 0000000..68baa2c --- /dev/null +++ b/plugins/fs/src/desktop.rs @@ -0,0 +1,44 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use std::path::PathBuf; + +use tauri::{AppHandle, Runtime}; + +use crate::{FilePath, OpenOptions}; + +/// Access to the file system APIs on desktop. +/// +/// Retrieved with [`crate::FsExt::fs`]. +pub struct Fs(pub(crate) AppHandle); + +fn path_or_err>(p: P) -> std::io::Result { + match p.into() { + FilePath::Path(p) => Ok(p), + FilePath::Url(u) if u.scheme() == "file" => u + .to_file_path() + .map_err(|_| std::io::Error::new(std::io::ErrorKind::InvalidInput, "invalid file URL")), + FilePath::Url(_) => Err(std::io::Error::new( + std::io::ErrorKind::InvalidInput, + "cannot use a URL to load files on desktop and iOS", + )), + } +} + +impl Fs { + /// Open a file. + /// + /// # Platform-specific + /// + /// - **iOS**: This method will automatically start accessing a security-scoped resource if the path is a file URL. + /// You must call `stop_accessing_security_scoped_resource` when you're done accessing the file. + pub fn open>( + &self, + path: P, + opts: OpenOptions, + ) -> std::io::Result { + let path = path_or_err(path)?; + std::fs::OpenOptions::from(opts).open(path) + } +} diff --git a/plugins/fs/src/error.rs b/plugins/fs/src/error.rs new file mode 100644 index 0000000..cb9685f --- /dev/null +++ b/plugins/fs/src/error.rs @@ -0,0 +1,51 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use std::path::PathBuf; + +use serde::{Serialize, Serializer}; + +/// Errors that can happen while using the file system plugin. +#[derive(Debug, thiserror::Error)] +#[non_exhaustive] +pub enum Error { + /// JSON serialization or deserialization error. + #[error(transparent)] + Json(#[from] serde_json::Error), + /// Error from the Tauri APIs, usually raised while resolving a path or a scope entry. + #[error(transparent)] + Tauri(#[from] tauri::Error), + /// Error from the underlying file system operation. + #[error(transparent)] + Io(#[from] std::io::Error), + /// The path is denied by the plugin scope or is not allowed by it. + #[error("forbidden path: {0}")] + PathForbidden(PathBuf), + /// Invalid glob pattern. + #[error("invalid glob pattern: {0}")] + GlobPattern(#[from] glob::PatternError), + /// Watcher error. + #[cfg(feature = "watch")] + #[error(transparent)] + Watch(#[from] notify::Error), + /// Error invoking the Android plugin implementation. + #[cfg(target_os = "android")] + #[error(transparent)] + PluginInvoke(#[from] tauri::plugin::mobile::PluginInvokeError), + /// The URL cannot be converted to a file system path. + #[error("URL is not a valid path")] + InvalidPathUrl, + /// The path is not safe to use, for instance because it traverses parent directories. + #[error("Unsafe PathBuf: {0}")] + UnsafePathBuf(&'static str), +} + +impl Serialize for Error { + fn serialize(&self, serializer: S) -> std::result::Result + where + S: Serializer, + { + serializer.serialize_str(self.to_string().as_ref()) + } +} diff --git a/plugins/fs/src/file_path.rs b/plugins/fs/src/file_path.rs new file mode 100644 index 0000000..6316a24 --- /dev/null +++ b/plugins/fs/src/file_path.rs @@ -0,0 +1,308 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use std::{ + convert::Infallible, + path::{Path, PathBuf}, + str::FromStr, +}; + +use serde::Serialize; +use tauri::path::SafePathBuf; + +use crate::{Error, Result}; + +/// Represents either a filesystem path or a URI pointing to a file +/// such as `file://` URIs or Android `content://` URIs. +#[derive(Debug, Serialize, Clone)] +#[serde(untagged)] +pub enum FilePath { + /// `file://` URIs or Android `content://` URIs. + Url(url::Url), + /// Regular [`PathBuf`] + Path(PathBuf), +} + +/// Represents either a safe filesystem path or a URI pointing to a file +/// such as `file://` URIs or Android `content://` URIs. +#[derive(Debug, Clone, Serialize)] +pub enum SafeFilePath { + /// `file://` URIs or Android `content://` URIs. + Url(url::Url), + /// Safe [`PathBuf`], see [`SafePathBuf``]. + Path(SafePathBuf), +} + +impl FilePath { + /// Get a reference to the contained [`Path`] if the variant is [`FilePath::Path`]. + /// + /// Use [`FilePath::into_path`] to try to convert the [`FilePath::Url`] variant as well. + #[inline] + pub fn as_path(&self) -> Option<&Path> { + match self { + Self::Url(_) => None, + Self::Path(p) => Some(p), + } + } + + /// Try to convert into [`PathBuf`] if possible. + /// + /// This calls [`Url::to_file_path`](url::Url::to_file_path) if the variant is [`FilePath::Url`], + /// otherwise returns the contained [PathBuf] as is. + #[inline] + pub fn into_path(self) -> Result { + match self { + Self::Url(url) => url.to_file_path().map_err(|_| Error::InvalidPathUrl), + Self::Path(p) => Ok(p), + } + } + + /// Takes the contained [`PathBuf`] if the variant is [`FilePath::Path`], + /// and when possible, converts Windows UNC paths to regular paths. + #[inline] + pub fn simplified(self) -> Self { + match self { + Self::Url(url) => Self::Url(url), + Self::Path(p) => Self::Path(dunce::simplified(&p).to_path_buf()), + } + } +} + +impl SafeFilePath { + /// Get a reference to the contained [`Path`] if the variant is [`SafeFilePath::Path`]. + /// + /// Use [`SafeFilePath::into_path`] to try to convert the [`SafeFilePath::Url`] variant as well. + #[inline] + pub fn as_path(&self) -> Option<&Path> { + match self { + Self::Url(_) => None, + Self::Path(p) => Some(p.as_ref()), + } + } + + /// Try to convert into [`PathBuf`] if possible. + /// + /// This calls [`Url::to_file_path`](url::Url::to_file_path) if the variant is [`SafeFilePath::Url`], + /// otherwise returns the contained [PathBuf] as is. + #[inline] + pub fn into_path(self) -> Result { + match self { + Self::Url(url) => url.to_file_path().map_err(|_| Error::InvalidPathUrl), + Self::Path(p) => Ok(p.as_ref().to_owned()), + } + } + + /// Takes the contained [`PathBuf`] if the variant is [`SafeFilePath::Path`], + /// and when possible, converts Windows UNC paths to regular paths. + #[inline] + pub fn simplified(self) -> Self { + match self { + Self::Url(url) => Self::Url(url), + Self::Path(p) => { + // Safe to unwrap since it was a safe file path already + Self::Path(SafePathBuf::new(dunce::simplified(p.as_ref()).to_path_buf()).unwrap()) + } + } + } +} + +impl std::fmt::Display for FilePath { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Url(u) => u.fmt(f), + Self::Path(p) => p.display().fmt(f), + } + } +} + +impl std::fmt::Display for SafeFilePath { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Url(u) => u.fmt(f), + Self::Path(p) => p.display().fmt(f), + } + } +} + +impl<'de> serde::Deserialize<'de> for FilePath { + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + struct FilePathVisitor; + + impl serde::de::Visitor<'_> for FilePathVisitor { + type Value = FilePath; + + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + formatter.write_str("a string representing an file URL or a path") + } + + fn visit_str(self, s: &str) -> std::result::Result + where + E: serde::de::Error, + { + FilePath::from_str(s).map_err(|e| { + serde::de::Error::invalid_value( + serde::de::Unexpected::Str(s), + &e.to_string().as_str(), + ) + }) + } + } + + deserializer.deserialize_str(FilePathVisitor) + } +} + +impl<'de> serde::Deserialize<'de> for SafeFilePath { + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + struct SafeFilePathVisitor; + + impl serde::de::Visitor<'_> for SafeFilePathVisitor { + type Value = SafeFilePath; + + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + formatter.write_str("a string representing an file URL or a path") + } + + fn visit_str(self, s: &str) -> std::result::Result + where + E: serde::de::Error, + { + SafeFilePath::from_str(s).map_err(|e| { + serde::de::Error::invalid_value( + serde::de::Unexpected::Str(s), + &e.to_string().as_str(), + ) + }) + } + } + + deserializer.deserialize_str(SafeFilePathVisitor) + } +} + +impl FromStr for FilePath { + type Err = Infallible; + fn from_str(s: &str) -> std::result::Result { + if let Ok(url) = url::Url::from_str(s) { + if url.scheme().len() != 1 { + return Ok(Self::Url(url)); + } + } + Ok(Self::Path(PathBuf::from(s))) + } +} + +impl FromStr for SafeFilePath { + type Err = Error; + fn from_str(s: &str) -> Result { + if let Ok(url) = url::Url::from_str(s) { + if url.scheme().len() != 1 { + return Ok(Self::Url(url)); + } + } + + SafePathBuf::new(s.into()) + .map(SafeFilePath::Path) + .map_err(Error::UnsafePathBuf) + } +} + +impl From for FilePath { + fn from(value: PathBuf) -> Self { + Self::Path(value) + } +} + +impl TryFrom for SafeFilePath { + type Error = Error; + fn try_from(value: PathBuf) -> Result { + SafePathBuf::new(value) + .map(SafeFilePath::Path) + .map_err(Error::UnsafePathBuf) + } +} + +impl From<&Path> for FilePath { + fn from(value: &Path) -> Self { + Self::Path(value.to_owned()) + } +} + +impl TryFrom<&Path> for SafeFilePath { + type Error = Error; + fn try_from(value: &Path) -> Result { + SafePathBuf::new(value.to_path_buf()) + .map(SafeFilePath::Path) + .map_err(Error::UnsafePathBuf) + } +} + +impl From<&PathBuf> for FilePath { + fn from(value: &PathBuf) -> Self { + Self::Path(value.to_owned()) + } +} + +impl TryFrom<&PathBuf> for SafeFilePath { + type Error = Error; + fn try_from(value: &PathBuf) -> Result { + SafePathBuf::new(value.to_owned()) + .map(SafeFilePath::Path) + .map_err(Error::UnsafePathBuf) + } +} + +impl From for FilePath { + fn from(value: url::Url) -> Self { + Self::Url(value) + } +} + +impl From for SafeFilePath { + fn from(value: url::Url) -> Self { + Self::Url(value) + } +} + +impl TryFrom for PathBuf { + type Error = Error; + fn try_from(value: FilePath) -> Result { + value.into_path() + } +} + +impl TryFrom for PathBuf { + type Error = Error; + fn try_from(value: SafeFilePath) -> Result { + value.into_path() + } +} + +impl From for FilePath { + fn from(value: SafeFilePath) -> Self { + match value { + SafeFilePath::Url(url) => FilePath::Url(url), + SafeFilePath::Path(p) => FilePath::Path(p.as_ref().to_owned()), + } + } +} + +impl TryFrom for SafeFilePath { + type Error = Error; + + fn try_from(value: FilePath) -> Result { + match value { + FilePath::Url(url) => Ok(SafeFilePath::Url(url)), + FilePath::Path(p) => SafePathBuf::new(p) + .map(SafeFilePath::Path) + .map_err(Error::UnsafePathBuf), + } + } +} diff --git a/plugins/fs/src/ios.rs b/plugins/fs/src/ios.rs new file mode 100644 index 0000000..fa89361 --- /dev/null +++ b/plugins/fs/src/ios.rs @@ -0,0 +1,142 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use serde::de::DeserializeOwned; +use tauri::{plugin::PluginApi, AppHandle, Runtime}; + +use crate::{FilePath, OpenOptions}; + +/// Access to the file system APIs on iOS. +/// +/// Opening a `file://` URL automatically starts accessing the matching security-scoped resource. +/// +/// Retrieved with [`crate::FsExt::fs`]. +pub struct Fs { + _phantom: std::marker::PhantomData R>, +} + +pub fn init( + _app: &AppHandle, + _api: PluginApi, +) -> crate::Result> { + Ok(Fs { + _phantom: std::marker::PhantomData, + }) +} + +impl Fs { + /// Open a file. + /// + /// # Platform-specific + /// + /// - **iOS**: This method will automatically start accessing a security-scoped resource if the path is a file URL. + /// You must call [`Self::stop_accessing_security_scoped_resource`] when you're done accessing the file. + pub fn open>( + &self, + path: P, + opts: OpenOptions, + ) -> std::io::Result { + use objc2_foundation::{NSString, NSURL}; + + match path.into() { + FilePath::Url(url) if url.scheme() == "file" => { + // Handle security-scoped URLs on iOS + let url_string = url.as_str(); + let url_nsstring = NSString::from_str(url_string); + + // Create NSURL from the URL string + // URLWithString may return None for invalid URLs, but file:// URLs should be valid + let ns_url = unsafe { NSURL::URLWithString(&url_nsstring) }; + if let Some(ns_url) = ns_url { + // Start accessing the security-scoped resource + // This is required for files outside the app's sandbox (e.g., from file picker) + // Note: We don't call stopAccessingSecurityScopedResource here because + // the file handle needs to remain accessible while the File is in use. + // The access will be automatically stopped when the app is backgrounded or terminated. + unsafe { + let success = ns_url.startAccessingSecurityScopedResource(); + if success { + log::debug!( + "Started accessing security-scoped resource for URL: {}", + url_string + ); + } else { + log::warn!( + "Failed to start accessing security-scoped resource for URL: {}", + url_string + ); + } + } + } else { + log::debug!("Failed to create NSURL from URL: {}, ignoring security-scoped resource access request", url_string); + } + + // Convert URL to path and open the file + let path = url.to_file_path().map_err(|_| { + std::io::Error::new(std::io::ErrorKind::InvalidInput, "invalid file URL") + })?; + std::fs::OpenOptions::from(opts).open(path) + } + FilePath::Url(_) => Err(std::io::Error::new( + std::io::ErrorKind::InvalidInput, + "cannot use a non-file URL to load files on iOS", + )), + FilePath::Path(p) => { + // Regular path, no security-scoped resource handling needed + std::fs::OpenOptions::from(opts).open(p) + } + } + } + + /// Stops accessing a security-scoped resource for the given file path or URL. + /// This should be called when you're done accessing a file that was opened + /// using a security-scoped URL (e.g., from a file picker). + /// + /// # Arguments + /// + /// * `path` - A file path or URL that was previously accessed via `startAccessingSecurityScopedResource` + /// + /// # Returns + /// + /// Returns `Ok(())` if successful, or an error if the path/URL is invalid or not a file URL. + pub fn stop_accessing_security_scoped_resource>( + &self, + path: P, + ) -> crate::Result<()> { + use objc2_foundation::{NSString, NSURL}; + + let file_path = path.into(); + let url_string = match file_path { + FilePath::Url(url) => { + if url.scheme() != "file" { + return Err(crate::Error::InvalidPathUrl); + } + url.as_str().to_string() + } + FilePath::Path(p) => { + // Convert path to file URL + url::Url::from_file_path(&p) + .map_err(|_| crate::Error::InvalidPathUrl)? + .as_str() + .to_string() + } + }; + + let url_nsstring = NSString::from_str(&url_string); + let ns_url = unsafe { NSURL::URLWithString(&url_nsstring) }; + if let Some(ns_url) = ns_url { + // Stop accessing the security-scoped resource + unsafe { + ns_url.stopAccessingSecurityScopedResource(); + } + } else { + return Err(crate::Error::Io(std::io::Error::new( + std::io::ErrorKind::InvalidInput, + "failed to create NSURL from URL", + ))); + } + + Ok(()) + } +} diff --git a/plugins/fs/src/lib.rs b/plugins/fs/src/lib.rs new file mode 100644 index 0000000..cac582b --- /dev/null +++ b/plugins/fs/src/lib.rs @@ -0,0 +1,632 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +//! Access the file system. +//! +//! ## Cargo features +//! +//! - **watch**: Enables the `watch` command backed by [`notify`](http://crates.io/crates/notify). + +// TODO(v3): consider redesign the API to implement automatic stopAccessingSecurityScopedResource on iOS +// this likely requires returning a handle to a resource so we can impl Drop for it + +#![doc( + html_logo_url = "https://github.com/tauri-apps/tauri/raw/dev/app-icon.png", + html_favicon_url = "https://github.com/tauri-apps/tauri/raw/dev/app-icon.png" +)] + +use std::io::Read; +#[cfg(target_os = "ios")] +use std::sync::Mutex; + +use serde::Deserialize; +use tauri::{ + ipc::ScopeObject, + plugin::{Builder as PluginBuilder, TauriPlugin}, + utils::{acl::Value, config::FsScope}, + AppHandle, DragDropEvent, Manager, RunEvent, Runtime, WindowEvent, +}; + +#[cfg(target_os = "android")] +mod android; +mod commands; +mod config; +#[cfg(desktop)] +mod desktop; +mod error; +mod file_path; +#[cfg(target_os = "ios")] +mod ios; +#[cfg(target_os = "android")] +mod models; +mod scope; +#[cfg(feature = "watch")] +mod watcher; + +#[cfg(target_os = "android")] +pub use android::Fs; +#[cfg(desktop)] +pub use desktop::Fs; +#[cfg(target_os = "ios")] +pub use ios::Fs; + +pub use error::Error; + +pub use file_path::FilePath; +pub use file_path::SafeFilePath; + +type Result = std::result::Result; + +/// Options and flags which can be used to configure how a file is opened. +/// +/// This builder exposes the ability to configure how a [`std::fs::File`] is opened and +/// what operations are permitted on the open file. Build it with [`OpenOptions::new`], +/// chain calls to the setter methods and pass it to [`Fs::open`]. +/// +/// The `read` option defaults to `true`, every other option defaults to `false`. +#[derive(Debug, Default, Clone, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct OpenOptions { + #[serde(default = "default_true")] + read: bool, + #[serde(default)] + write: bool, + #[serde(default)] + append: bool, + #[serde(default)] + truncate: bool, + #[serde(default)] + create: bool, + #[serde(default)] + create_new: bool, + #[serde(default)] + #[allow(unused)] + mode: Option, + #[serde(default)] + #[allow(unused)] + custom_flags: Option, +} + +fn default_true() -> bool { + true +} + +impl From for std::fs::OpenOptions { + fn from(open_options: OpenOptions) -> Self { + let mut opts = std::fs::OpenOptions::new(); + + #[cfg(unix)] + { + use std::os::unix::fs::OpenOptionsExt; + if let Some(mode) = open_options.mode { + opts.mode(mode); + } + if let Some(flags) = open_options.custom_flags { + opts.custom_flags(flags); + } + } + + opts.read(open_options.read) + .write(open_options.write) + .create(open_options.create) + .append(open_options.append) + .truncate(open_options.truncate) + .create_new(open_options.create_new); + + opts + } +} + +impl OpenOptions { + /// Creates a blank new set of options ready for configuration. + /// + /// All options are initially set to `false`. + /// + /// # Examples + /// + /// ```rust,no_run + /// use std::path::Path; + /// use tauri_plugin_fs::{FsExt, OpenOptions}; + /// + /// tauri::Builder::default() + /// .setup(|app| { + /// let mut options = OpenOptions::new(); + /// options.read(true); + /// let file = app.fs().open(Path::new("foo.txt"), options)?; + /// Ok(()) + /// }); + /// ``` + #[must_use] + pub fn new() -> Self { + Self::default() + } + + /// Sets the option for read access. + /// + /// This option, when true, will indicate that the file should be + /// `read`-able if opened. + /// + /// # Examples + /// + /// ```rust,no_run + /// use std::path::Path; + /// use tauri_plugin_fs::{FsExt, OpenOptions}; + /// + /// tauri::Builder::default() + /// .setup(|app| { + /// let mut options = OpenOptions::new(); + /// options.read(true); + /// let file = app.fs().open(Path::new("foo.txt"), options)?; + /// Ok(()) + /// }); + /// ``` + pub fn read(&mut self, read: bool) -> &mut Self { + self.read = read; + self + } + + /// Sets the option for write access. + /// + /// This option, when true, will indicate that the file should be + /// `write`-able if opened. + /// + /// If the file already exists, any write calls on it will overwrite its + /// contents, without truncating it. + /// + /// # Examples + /// + /// ```rust,no_run + /// use std::path::Path; + /// use tauri_plugin_fs::{FsExt, OpenOptions}; + /// + /// tauri::Builder::default() + /// .setup(|app| { + /// let mut options = OpenOptions::new(); + /// options.write(true); + /// let file = app.fs().open(Path::new("foo.txt"), options)?; + /// Ok(()) + /// }); + /// ``` + pub fn write(&mut self, write: bool) -> &mut Self { + self.write = write; + self + } + + /// Sets the option for the append mode. + /// + /// This option, when true, means that writes will append to a file instead + /// of overwriting previous contents. + /// Note that setting `.write(true).append(true)` has the same effect as + /// setting only `.append(true)`. + /// + /// Append mode guarantees that writes will be positioned at the current end of file, + /// even when there are other processes or threads appending to the same file. This is + /// unlike [seek]\([SeekFrom]::[End]\(0)) followed by `write()`, which + /// has a race between seeking and writing during which another writer can write, with + /// our `write()` overwriting their data. + /// + /// Keep in mind that this does not necessarily guarantee that data appended by + /// different processes or threads does not interleave. The amount of data accepted a + /// single `write()` call depends on the operating system and file system. A + /// successful `write()` is allowed to write only part of the given data, so even if + /// you're careful to provide the whole message in a single call to `write()`, there + /// is no guarantee that it will be written out in full. If you rely on the filesystem + /// accepting the message in a single write, make sure that all data that belongs + /// together is written in one operation. This can be done by concatenating strings + /// before passing them to [`write()`]. + /// + /// If a file is opened with both read and append access, beware that after + /// opening, and after every write, the position for reading may be set at the + /// end of the file. So, before writing, save the current position (using + /// [Seek]::[stream_position]), and restore it before the next read. + /// + /// ## Note + /// + /// This function doesn't create the file if it doesn't exist. Use the + /// [`OpenOptions::create`] method to do so. + /// + /// [`write()`]: std::io::Write::write "io::Write::write" + /// [`flush()`]: std::io::Write::flush "io::Write::flush" + /// [Seek]: std::io::Seek "io::Seek" + /// [stream_position]: std::io::Seek::stream_position "io::Seek::stream_position" + /// [seek]: std::io::Seek::seek "io::Seek::seek" + /// [SeekFrom]: std::io::SeekFrom "io::SeekFrom" + /// [Current]: std::io::SeekFrom::Current "io::SeekFrom::Current" + /// [End]: std::io::SeekFrom::End "io::SeekFrom::End" + /// + /// # Examples + /// + /// ```rust,no_run + /// use std::path::Path; + /// use tauri_plugin_fs::{FsExt, OpenOptions}; + /// + /// tauri::Builder::default() + /// .setup(|app| { + /// let mut options = OpenOptions::new(); + /// options.append(true); + /// let file = app.fs().open(Path::new("foo.txt"), options)?; + /// Ok(()) + /// }); + /// ``` + pub fn append(&mut self, append: bool) -> &mut Self { + self.append = append; + self + } + + /// Sets the option for truncating a previous file. + /// + /// If a file is successfully opened with this option set it will truncate + /// the file to 0 length if it already exists. + /// + /// The file must be opened with write access for truncate to work. + /// + /// # Examples + /// + /// ```rust,no_run + /// use std::path::Path; + /// use tauri_plugin_fs::{FsExt, OpenOptions}; + /// + /// tauri::Builder::default() + /// .setup(|app| { + /// let mut options = OpenOptions::new(); + /// options.write(true).truncate(true); + /// let file = app.fs().open(Path::new("foo.txt"), options)?; + /// Ok(()) + /// }); + /// ``` + pub fn truncate(&mut self, truncate: bool) -> &mut Self { + self.truncate = truncate; + self + } + + /// Sets the option to create a new file, or open it if it already exists. + /// + /// In order for the file to be created, [`OpenOptions::write`] or + /// [`OpenOptions::append`] access must be used. + /// + /// + /// # Examples + /// + /// ```rust,no_run + /// use std::path::Path; + /// use tauri_plugin_fs::{FsExt, OpenOptions}; + /// + /// tauri::Builder::default() + /// .setup(|app| { + /// let mut options = OpenOptions::new(); + /// options.write(true).create(true); + /// let file = app.fs().open(Path::new("foo.txt"), options)?; + /// Ok(()) + /// }); + /// ``` + pub fn create(&mut self, create: bool) -> &mut Self { + self.create = create; + self + } + + /// Sets the option to create a new file, failing if it already exists. + /// + /// No file is allowed to exist at the target location, also no (dangling) symlink. In this + /// way, if the call succeeds, the file returned is guaranteed to be new. + /// If a file exists at the target location, creating a new file will fail with [`AlreadyExists`] + /// or another error based on the situation. See [`std::fs::OpenOptions::open`] for a + /// non-exhaustive list of likely errors. + /// + /// This option is useful because it is atomic. Otherwise between checking + /// whether a file exists and creating a new one, the file may have been + /// created by another process (a TOCTOU race condition / attack). + /// + /// If `.create_new(true)` is set, [`.create()`] and [`.truncate()`] are + /// ignored. + /// + /// The file must be opened with write or append access in order to create + /// a new file. + /// + /// [`.create()`]: OpenOptions::create + /// [`.truncate()`]: OpenOptions::truncate + /// [`AlreadyExists`]: std::io::ErrorKind::AlreadyExists + /// + /// # Examples + /// + /// ```rust,no_run + /// use std::path::Path; + /// use tauri_plugin_fs::{FsExt, OpenOptions}; + /// + /// tauri::Builder::default() + /// .setup(|app| { + /// let mut options = OpenOptions::new(); + /// options.write(true).create_new(true); + /// let file = app.fs().open(Path::new("foo.txt"), options)?; + /// Ok(()) + /// }); + /// ``` + pub fn create_new(&mut self, create_new: bool) -> &mut Self { + self.create_new = create_new; + self + } +} + +#[cfg(unix)] +impl std::os::unix::fs::OpenOptionsExt for OpenOptions { + fn custom_flags(&mut self, flags: i32) -> &mut Self { + self.custom_flags.replace(flags); + self + } + + fn mode(&mut self, mode: u32) -> &mut Self { + self.mode.replace(mode); + self + } +} + +impl OpenOptions { + #[cfg(target_os = "android")] + fn android_mode(&self) -> String { + let mut mode = String::new(); + + if self.read { + mode.push('r'); + } + if self.write { + mode.push('w'); + } + if self.truncate { + mode.push('t'); + } + if self.append { + mode.push('a'); + } + + mode + } +} + +impl Fs { + /// Reads the entire contents of a file into a string. + /// + /// The file is opened in read-only mode with [`Fs::open`]. + /// + /// # Errors + /// + /// Returns an error if `path` cannot be opened for reading or if its + /// contents are not valid UTF-8. + pub fn read_to_string>(&self, path: P) -> std::io::Result { + let mut s = String::new(); + self.open( + path, + OpenOptions { + read: true, + ..Default::default() + }, + )? + .read_to_string(&mut s)?; + Ok(s) + } + + /// Reads the entire contents of a file into a bytes vector. + /// + /// The file is opened in read-only mode with [`Fs::open`]. + /// + /// # Errors + /// + /// Returns an error if `path` cannot be opened for reading. + pub fn read>(&self, path: P) -> std::io::Result> { + let mut buf = Vec::new(); + self.open( + path, + OpenOptions { + read: true, + ..Default::default() + }, + )? + .read_to_end(&mut buf)?; + Ok(buf) + } +} + +// implement ScopeObject here instead of in the scope module because it is also used on the build script +// and we don't want to add tauri as a build dependency +impl ScopeObject for scope::Entry { + type Error = Error; + fn deserialize( + app: &AppHandle, + raw: Value, + ) -> std::result::Result { + let path = serde_json::from_value(raw.into()).map(|raw| match raw { + scope::EntryRaw::Value(path) => path, + scope::EntryRaw::Object { path } => path, + })?; + + match app.path().parse(path) { + Ok(path) => Ok(Self { path: Some(path) }), + #[cfg(not(target_os = "android"))] + Err(tauri::Error::UnknownPath) => Ok(Self { path: None }), + Err(err) => Err(err.into()), + } + } +} + +pub(crate) struct Scope { + pub(crate) scope: tauri::fs::Scope, + pub(crate) require_literal_leading_dot: Option, +} + +/// Tracks which paths have active security-scoped resource access on iOS. +#[cfg(target_os = "ios")] +pub(crate) struct SecurityScopedResources { + /// Set of file URLs that are currently accessing security-scoped resources. + /// The key is the URL string representation. + pub(crate) active_urls: Mutex>, +} + +#[cfg(target_os = "ios")] +impl SecurityScopedResources { + pub(crate) fn new() -> Self { + Self { + active_urls: Mutex::new(std::collections::HashSet::new()), + } + } + + pub(crate) fn is_tracked_manually(&self, url: &str) -> bool { + self.active_urls.lock().unwrap().contains(url) + } + + pub(crate) fn track_manually(&self, url: String) { + self.active_urls.lock().unwrap().insert(url); + } + + pub(crate) fn remove(&self, url: &str) { + self.active_urls.lock().unwrap().remove(url); + } +} + +#[cfg(not(target_os = "ios"))] +pub(crate) struct SecurityScopedResources; + +#[cfg(not(target_os = "ios"))] +impl SecurityScopedResources { + pub(crate) fn new() -> Self { + Self + } + + #[allow(dead_code)] // Used on iOS, but not on other platforms + pub(crate) fn is_tracked_manually(&self, _url: &str) -> bool { + false + } + + #[allow(dead_code)] // Used on iOS, but not on other platforms + pub(crate) fn track_manually(&self, _url: String) {} + + #[allow(dead_code)] // Used on iOS, but not on other platforms + pub(crate) fn remove(&self, _url: &str) {} +} + +/// Extension trait implemented by every [`Manager`] (the app handle, windows, webviews, ...) +/// to access the file system plugin APIs. +/// +/// # Examples +/// +/// ```rust,no_run +/// use std::path::Path; +/// use tauri::Runtime; +/// use tauri_plugin_fs::FsExt; +/// +/// fn setup(app: &tauri::App) -> Result<(), Box> { +/// // allow the app to access a directory that is not part of the static scope +/// app.fs_scope().allow_directory(Path::new("/path/to/directory"), true)?; +/// +/// let contents = app.fs().read_to_string(Path::new("/path/to/directory/file.txt"))?; +/// println!("{contents}"); +/// +/// Ok(()) +/// } +/// ``` +pub trait FsExt { + /// Returns the file system scope, which can be used to dynamically + /// allow or deny paths at runtime. + /// + /// # Panics + /// + /// Panics if the plugin is not registered in the app. + /// Use [`FsExt::try_fs_scope`] if the plugin might not be registered. + fn fs_scope(&self) -> tauri::fs::Scope; + + /// Returns the file system scope, or `None` if the plugin is not registered in the app. + fn try_fs_scope(&self) -> Option; + + /// Cross platform file system APIs that also support manipulating Android files. + fn fs(&self) -> &Fs; +} + +impl> FsExt for T { + fn fs_scope(&self) -> tauri::fs::Scope { + self.state::().scope.clone() + } + + fn try_fs_scope(&self) -> Option { + self.try_state::().map(|s| s.scope.clone()) + } + + fn fs(&self) -> &Fs { + self.state::>().inner() + } +} + +/// Initializes the plugin. +pub fn init() -> TauriPlugin> { + PluginBuilder::>::new("fs") + .invoke_handler(tauri::generate_handler![ + commands::create, + commands::open, + commands::copy_file, + commands::mkdir, + commands::read_dir, + commands::read, + commands::read_file, + commands::read_text_file, + commands::read_text_file_lines, + commands::read_text_file_lines_next, + commands::remove, + commands::rename, + commands::seek, + commands::stat, + commands::lstat, + commands::fstat, + commands::truncate, + commands::ftruncate, + commands::write, + commands::write_file, + commands::write_text_file, + commands::exists, + commands::size, + commands::start_accessing_security_scoped_resource, + commands::stop_accessing_security_scoped_resource, + #[cfg(feature = "watch")] + watcher::watch, + ]) + .setup(|app, api| { + let scope = Scope { + require_literal_leading_dot: api + .config() + .as_ref() + .and_then(|c| c.require_literal_leading_dot), + scope: tauri::fs::Scope::new(app, &FsScope::default())?, + }; + + #[cfg(target_os = "android")] + { + let fs = android::init(app, api)?; + app.manage(fs); + } + #[cfg(target_os = "ios")] + { + let fs = ios::init(app, api)?; + app.manage(fs); + } + #[cfg(desktop)] + app.manage(Fs(app.clone())); + + app.manage(scope); + app.manage(SecurityScopedResources::new()); + Ok(()) + }) + .on_event(|app, event| { + if let RunEvent::WindowEvent { + label: _, + event: WindowEvent::DragDrop(DragDropEvent::Drop { paths, position: _ }), + .. + } = event + { + let scope = app.fs_scope(); + for path in paths { + if path.is_file() { + let _ = scope.allow_file(path); + } else { + let _ = scope.allow_directory(path, true); + } + } + } + }) + .build() +} diff --git a/plugins/fs/src/models.rs b/plugins/fs/src/models.rs new file mode 100644 index 0000000..b9edc2c --- /dev/null +++ b/plugins/fs/src/models.rs @@ -0,0 +1,18 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, Default, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct GetFileDescriptorPayload { + pub uri: String, + pub mode: String, +} + +#[derive(Debug, Clone, Default, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct GetFileDescriptorResponse { + pub fd: Option, +} diff --git a/plugins/fs/src/scope.rs b/plugins/fs/src/scope.rs new file mode 100644 index 0000000..7914706 --- /dev/null +++ b/plugins/fs/src/scope.rs @@ -0,0 +1,19 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use std::path::PathBuf; + +use serde::Deserialize; + +#[derive(Debug)] +pub struct Entry { + pub path: Option, +} + +#[derive(Deserialize)] +#[serde(untagged)] +pub(crate) enum EntryRaw { + Value(PathBuf), + Object { path: PathBuf }, +} diff --git a/plugins/fs/src/watcher.rs b/plugins/fs/src/watcher.rs new file mode 100644 index 0000000..de9a85d --- /dev/null +++ b/plugins/fs/src/watcher.rs @@ -0,0 +1,104 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use notify::{Config, RecommendedWatcher, RecursiveMode, Watcher}; +use notify_debouncer_full::{new_debouncer, DebouncedEvent, Debouncer, RecommendedCache}; +use serde::Deserialize; +use tauri::{ + ipc::{Channel, CommandScope, GlobalScope}, + path::BaseDirectory, + Manager, Resource, ResourceId, Runtime, Webview, +}; + +use std::time::Duration; + +use crate::{ + commands::{resolve_path, CommandResult}, + scope::Entry, + SafeFilePath, +}; + +#[allow(unused)] +enum WatcherKind { + Debouncer(Debouncer), + Watcher(RecommendedWatcher), +} + +impl Resource for WatcherKind {} + +#[derive(Clone, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct WatchOptions { + base_dir: Option, + #[serde(default)] + recursive: bool, + delay_ms: Option, +} + +#[tauri::command] +pub fn watch( + webview: Webview, + paths: Vec, + options: WatchOptions, + on_event: Channel, + global_scope: GlobalScope, + command_scope: CommandScope, +) -> CommandResult { + let resolved_paths = paths + .into_iter() + .map(|path| { + resolve_path( + "watch", + &webview, + &global_scope, + &command_scope, + path, + options.base_dir, + ) + }) + .collect::>>()?; + + let recursive_mode = if options.recursive { + RecursiveMode::Recursive + } else { + RecursiveMode::NonRecursive + }; + + let watcher_kind = if let Some(delay) = options.delay_ms { + let mut debouncer = new_debouncer( + Duration::from_millis(delay), + None, + move |events: Result, Vec>| { + if let Ok(events) = events { + for event in events { + // TODO: Should errors be emitted too? + let _ = on_event.send(event.event); + } + } + }, + )?; + for path in &resolved_paths { + debouncer.watch(path, recursive_mode)?; + } + WatcherKind::Debouncer(debouncer) + } else { + let mut watcher = RecommendedWatcher::new( + move |event| { + if let Ok(event) = event { + // TODO: Should errors be emitted too? + let _ = on_event.send(event); + } + }, + Config::default(), + )?; + for path in &resolved_paths { + watcher.watch(path, recursive_mode)?; + } + WatcherKind::Watcher(watcher) + }; + + let rid = webview.resources_table().add(watcher_kind); + + Ok(rid) +} diff --git a/plugins/fs/tsconfig.json b/plugins/fs/tsconfig.json new file mode 100644 index 0000000..4374468 --- /dev/null +++ b/plugins/fs/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "rootDir": "guest-js" + }, + "include": ["guest-js/*.ts"] +} diff --git a/plugins/geolocation/CHANGELOG.md b/plugins/geolocation/CHANGELOG.md new file mode 100644 index 0000000..cbb3cef --- /dev/null +++ b/plugins/geolocation/CHANGELOG.md @@ -0,0 +1,79 @@ +# Changelog + +## [2.3.3] + +- [`db9c5998`](https://github.com/tauri-apps/plugins-workspace/commit/db9c5998feff9384f9cbbefcbe0d45937c00a1fc) ([#3531](https://github.com/tauri-apps/plugins-workspace/pull/3531) by [@Legend-Master](https://github.com/tauri-apps/plugins-workspace/../../Legend-Master)) Fix missing `consumer-rules.pro` on Gradle v9. +- [`f8053e65`](https://github.com/tauri-apps/plugins-workspace/commit/f8053e659e4ccd85c1f52833411ff8417cbc5e69) ([#3527](https://github.com/tauri-apps/plugins-workspace/pull/3527) by [@Legend-Master](https://github.com/tauri-apps/plugins-workspace/../../Legend-Master)) Documented Cargo feature flags in each plugin's crate-level documentation. + +## \[2.3.2] + +- [`93426f85`](https://github.com/tauri-apps/plugins-workspace/commit/93426f85120f49beb9f40222bff45185a32d54a9) Fixed an issue that caused docs.rs builds to fail. No user facing changes. + +## \[2.3.1] + +- [`6c9b61fb`](https://github.com/tauri-apps/plugins-workspace/commit/6c9b61fb658145d13893626112fc489f7458aa17) ([#3039](https://github.com/tauri-apps/plugins-workspace/pull/3039) by [@FabianLars](https://github.com/tauri-apps/plugins-workspace/../../FabianLars)) On Android, updated compileSdk to 36. +- [`e933acb0`](https://github.com/tauri-apps/plugins-workspace/commit/e933acb0044b4d49053eae6492b542351160e66a) ([#3010](https://github.com/tauri-apps/plugins-workspace/pull/3010) by [@FabianLars](https://github.com/tauri-apps/plugins-workspace/../../FabianLars)) On Android, use the `timeout` value for `setMinUpdateIntervalMillis`, `setMaxUpdateDelayMillis` and `setIntervalMillis` instead of just `minUpdateInterval`. + +## \[2.3.0] + +- [`f209b2f2`](https://github.com/tauri-apps/plugins-workspace/commit/f209b2f23cb29133c97ad5961fb46ef794dbe063) ([#2804](https://github.com/tauri-apps/plugins-workspace/pull/2804) by [@renovate](https://github.com/tauri-apps/plugins-workspace/../../renovate)) Updated tauri to 2.6 + +## \[2.2.5] + +- [`f634e524`](https://github.com/tauri-apps/plugins-workspace/commit/f634e5248ebe428f8305a59f74c13fc15147fb8e) This is an "empty" release to update the plugins' source files on crates.io and docs.rs. This should fix docs.rs build failures for projects using tauri plugins as dependencies. + +## \[2.2.4] + +- [`a1b3fa27`](https://github.com/tauri-apps/plugins-workspace/commit/a1b3fa27f11022c9b6622b4fab12d93239eb05de) ([#2515](https://github.com/tauri-apps/plugins-workspace/pull/2515) by [@FabianLars](https://github.com/tauri-apps/plugins-workspace/../../FabianLars)) Re-exported the `Geolocation`, `Haptics`, `Notification`, and `Os` structs so that they show up on docs.rs. + +## \[2.2.3] + +- [`406e6f48`](https://github.com/tauri-apps/plugins-workspace/commit/406e6f484cdc13d35c50fb949f7489ca9eeccc44) ([#2323](https://github.com/tauri-apps/plugins-workspace/pull/2323) by [@FabianLars](https://github.com/tauri-apps/plugins-workspace/../../FabianLars)) Fixed an issue that caused build failures when the `haptics` or `geolocation` plugin was used without their `specta` feature flag enabled. + +## \[2.2.2] + +- [`c9c13a0f`](https://github.com/tauri-apps/plugins-workspace/commit/c9c13a0fe7cdaac223843f5ba33176252f8e22f5) ([#2316](https://github.com/tauri-apps/plugins-workspace/pull/2316) by [@FabianLars](https://github.com/tauri-apps/plugins-workspace/../../FabianLars)) **Breaking change:** `specta` integration is now behind a `specta` feature flag like in Tauri. +- [`c9c13a0f`](https://github.com/tauri-apps/plugins-workspace/commit/c9c13a0fe7cdaac223843f5ba33176252f8e22f5) ([#2316](https://github.com/tauri-apps/plugins-workspace/pull/2316) by [@FabianLars](https://github.com/tauri-apps/plugins-workspace/../../FabianLars)) Unlock and widen `specta` version range to match Tauri. No API changes. + +## \[2.2.1] + +- [`fb67ab2b`](https://github.com/tauri-apps/plugins-workspace/commit/fb67ab2b926502bfc20d6b43fbdd156691ea8526) ([#2281](https://github.com/tauri-apps/plugins-workspace/pull/2281) by [@FabianLars](https://github.com/tauri-apps/plugins-workspace/../../FabianLars)) Added `specta-util` to fix a "dependency not found" compilation error. + +## \[2.2.0] + +- [`3a79266b`](https://github.com/tauri-apps/plugins-workspace/commit/3a79266b8cf96a55b1ae6339d725567d45a44b1d) ([#2173](https://github.com/tauri-apps/plugins-workspace/pull/2173) by [@FabianLars](https://github.com/tauri-apps/plugins-workspace/../../FabianLars)) Bumped all plugins to `v2.2.0`. From now, the versions for the Rust and JavaScript packages of each plugin will be in sync with each other. + +## \[2.0.1] + +- [`a1a82208`](https://github.com/tauri-apps/plugins-workspace/commit/a1a82208ed4ab87f83310be0dc95428aec9ab241) ([#1873](https://github.com/tauri-apps/plugins-workspace/pull/1873) by [@lucasfernog](https://github.com/tauri-apps/plugins-workspace/../../lucasfernog)) Downgrade MSRV to 1.77.2 to support Windows 7. + +## \[2.0.0] + +- [`e2c4dfb6`](https://github.com/tauri-apps/plugins-workspace/commit/e2c4dfb6af43e5dd8d9ceba232c315f5febd55c1) Update to tauri v2 stable release. + +## \[2.0.0-rc.2] + +- [`60765694`](https://github.com/tauri-apps/plugins-workspace/commit/60765694f54875e22b8eb70b1d2e32dbf0c585c7) ([#1773](https://github.com/tauri-apps/plugins-workspace/pull/1773) by [@lucasfernog](https://github.com/tauri-apps/plugins-workspace/../../lucasfernog)) Update API to match other plugins. + +## \[2.0.0-rc.1] + +- [`e2e97db5`](https://github.com/tauri-apps/plugins-workspace/commit/e2e97db51983267f5be84d4f6f0278d58834d1f5) ([#1701](https://github.com/tauri-apps/plugins-workspace/pull/1701) by [@lucasfernog](https://github.com/tauri-apps/plugins-workspace/../../lucasfernog)) Use `PermissionState` from the `tauri` crate, which now also includes a "prompt with rationale" variant for Android (returned when your app must explain to the user why it needs the permission). +- [`e2e97db5`](https://github.com/tauri-apps/plugins-workspace/commit/e2e97db51983267f5be84d4f6f0278d58834d1f5) ([#1701](https://github.com/tauri-apps/plugins-workspace/pull/1701) by [@lucasfernog](https://github.com/tauri-apps/plugins-workspace/../../lucasfernog)) Update to tauri 2.0.0-rc.8 + +## \[2.0.0-rc.2] + +- [`b9147758`](https://github.com/tauri-apps/plugins-workspace/commit/b914775898c2bee7ceb20bd17ee595005cd17a64) ([#1679](https://github.com/tauri-apps/plugins-workspace/pull/1679) by [@lucasfernog](https://github.com/tauri-apps/plugins-workspace/../../lucasfernog)) Explicitly set a minimum macOS version for the Swift package. + +## \[2.0.0-rc.1] + +- [`5d170a54`](https://github.com/tauri-apps/plugins-workspace/commit/5d170a5444982dcc14135f6f1fc3e5da359f0eb0) ([#1671](https://github.com/tauri-apps/plugins-workspace/pull/1671) by [@lucasfernog](https://github.com/tauri-apps/plugins-workspace/../../lucasfernog)) Update to tauri 2.0.0-rc.3. + +### changes + +- [`6b079cfd`](https://github.com/tauri-apps/plugins-workspace/commit/6b079cfdd107c94abc2c7300f6af00bac3ff4040) ([#1649](https://github.com/tauri-apps/plugins-workspace/pull/1649) by [@ahqsoftwares](https://github.com/tauri-apps/plugins-workspace/../../ahqsoftwares)) Remove targetSdk from build.kts files as it is deprecated and will be removed from DSL v9.0 + +## \[2.0.0-rc.0] + +- [`9606089b`](https://github.com/tauri-apps/plugins-workspace/commit/9606089b2add4a17f80ed5a09d59ce94824bd672) ([#1599](https://github.com/tauri-apps/plugins-workspace/pull/1599)) Initial release. +- [`9887d1`](https://github.com/tauri-apps/plugins-workspace/commit/9887d14bd0e971c4c0f5c1188fc4005d3fc2e29e) Update to tauri RC. + tauri-apps/plugins-workspace/commit/9887d14bd0e971c4c0f5c1188fc4005d3fc2e29e) Update to tauri RC. diff --git a/plugins/geolocation/Cargo.toml b/plugins/geolocation/Cargo.toml new file mode 100644 index 0000000..3c2a508 --- /dev/null +++ b/plugins/geolocation/Cargo.toml @@ -0,0 +1,37 @@ +[package] +name = "tauri-plugin-geolocation" +description = "Get and track the device's current position" +version = "2.3.3" +edition = { workspace = true } +authors = { workspace = true } +license = { workspace = true } +repository = { workspace = true } +links = "tauri-plugin-geolocation" +exclude = ["/guest-js", "/package.json", "/rollup.config.js", "/tsconfig.json"] + +[package.metadata.docs.rs] +targets = ["x86_64-linux-android"] + +[package.metadata.platforms.support] +windows = { level = "none", notes = "" } +linux = { level = "none", notes = "" } +macos = { level = "none", notes = "" } +android = { level = "full", notes = "" } +ios = { level = "full", notes = "" } + +[build-dependencies] +tauri-plugin = { workspace = true, features = ["build"] } + +[dependencies] +serde = { workspace = true } +serde_json = { workspace = true } +tauri = { workspace = true } +log = { workspace = true } +thiserror = { workspace = true } +specta = { workspace = true, optional = true } + +[target.'cfg(target_os = "ios")'.dependencies] +tauri = { workspace = true, features = ["wry"] } + +[features] +specta = ["dep:specta", "tauri/specta"] diff --git a/plugins/geolocation/LICENSE.spdx b/plugins/geolocation/LICENSE.spdx new file mode 100644 index 0000000..cdd0df5 --- /dev/null +++ b/plugins/geolocation/LICENSE.spdx @@ -0,0 +1,20 @@ +SPDXVersion: SPDX-2.1 +DataLicense: CC0-1.0 +PackageName: tauri +DataFormat: SPDXRef-1 +PackageSupplier: Organization: The Tauri Programme in the Commons Conservancy +PackageHomePage: https://tauri.app +PackageLicenseDeclared: Apache-2.0 +PackageLicenseDeclared: MIT +PackageCopyrightText: 2019-2022, The Tauri Programme in the Commons Conservancy +PackageSummary: Tauri is a rust project that enables developers to make secure +and small desktop applications using a web frontend. + +PackageComment: The package includes the following libraries; see +Relationship information. + +Created: 2019-05-20T09:00:00Z +PackageDownloadLocation: git://github.com/tauri-apps/tauri +PackageDownloadLocation: git+https://github.com/tauri-apps/tauri.git +PackageDownloadLocation: git+ssh://github.com/tauri-apps/tauri.git +Creator: Person: Daniel Thompson-Yvetot \ No newline at end of file diff --git a/plugins/geolocation/LICENSE_APACHE-2.0 b/plugins/geolocation/LICENSE_APACHE-2.0 new file mode 100644 index 0000000..4947287 --- /dev/null +++ b/plugins/geolocation/LICENSE_APACHE-2.0 @@ -0,0 +1,177 @@ + + 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 \ No newline at end of file diff --git a/plugins/geolocation/LICENSE_MIT b/plugins/geolocation/LICENSE_MIT new file mode 100644 index 0000000..4d75472 --- /dev/null +++ b/plugins/geolocation/LICENSE_MIT @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2017 - Present Tauri Apps 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. \ No newline at end of file diff --git a/plugins/geolocation/README.md b/plugins/geolocation/README.md new file mode 100644 index 0000000..3de41ef --- /dev/null +++ b/plugins/geolocation/README.md @@ -0,0 +1,173 @@ +![geolocation](https://github.com/tauri-apps/plugins-workspace/raw/v2/plugins/geolocation/banner.png) + +This plugin provides APIs for getting and tracking the device's current position, including information about altitude, heading, and speed (if available). + +| Platform | Supported | +| -------- | --------- | +| Linux | x | +| Windows | x | +| macOS | x | +| Android | ✓ | +| iOS | ✓ | + +## Install + +_This plugin requires a Rust version of at least **1.77.2**_ + +There are three general methods of installation that we can recommend. + +1. Use crates.io and npm (easiest, and requires you to trust that our publishing pipeline worked) +2. Pull sources directly from Github using git tags / revision hashes (most secure) +3. Git submodule install this repo in your tauri project and then use file protocol to ingest the source (most secure, but inconvenient to use) + +Install the Core plugin by adding the following to your `Cargo.toml` file: + +`src-tauri/Cargo.toml` + +```toml +[dependencies] +tauri-plugin-geolocation = "2.0.0" +# alternatively with Git: +tauri-plugin-geolocation = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v2" } +``` + +You can install the JavaScript Guest bindings using your preferred JavaScript package manager: + + + +```sh +pnpm add @tauri-apps/plugin-geolocation +# or +npm add @tauri-apps/plugin-geolocation +# or +yarn add @tauri-apps/plugin-geolocation +``` + +## Setting up + +### iOS + +Apple requires privacy descriptions to be specified in `Info.plist` for location information: + +- `NSLocationWhenInUseDescription` + +### Android + +This plugin automatically adds the following permissions to your `AndroidManifest.xml` file: + +```xml + + +``` + +If your app requires GPS functionality to function, **you** should add the following to your `AndroidManifest.xml` file: + +```xml + +``` + +The Google Play Store uses this property to decide whether it should show the app to devices without GPS capabilities. + +## Usage + +First you need to register the core plugin with Tauri: + +`src-tauri/src/lib.rs` + +```rust +fn main() { + tauri::Builder::default() + .plugin(tauri_plugin_geolocation::init()) + .run(tauri::generate_context!()) + .expect("error while running tauri application"); +} +``` + +Then, for instance, grant the plugin the permission to check or request permissions from the user and to read the device position + +`src-tauri/capabilities/default.json` + +```json + "permissions": [ + "core:default", + "geolocation:allow-check-permissions", + "geolocation:allow-request-permissions", + "geolocation:allow-get-current-position", + "geolocation:allow-watch-position", + ] +``` + +Afterwards all the plugin's APIs are available through the JavaScript guest bindings: + +```javascript +import { + checkPermissions, + requestPermissions, + getCurrentPosition, + watchPosition +} from '@tauri-apps/plugin-geolocation' + +let permissions = await checkPermissions() +if ( + permissions.location === 'prompt' + || permissions.location === 'prompt-with-rationale' +) { + permissions = await requestPermissions(['location']) +} + +if (permissions.location === 'granted') { + const pos = await getCurrentPosition() + + await watchPosition( + { enableHighAccuracy: true, timeout: 10000, maximumAge: 0 }, + (pos) => { + console.log(pos) + } + ) +} +``` + +## Contributing + +PRs accepted. Please make sure to read the Contributing Guide before making a pull request. + +## Contributed By + + + + + + + + +
+ + CrabNebula + + + + Rescue.co + +
+ +## Partners + + + + + + + +
+ + CrabNebula + +
+ +For the complete list of sponsors please visit our [website](https://tauri.app#sponsors) and [Open Collective](https://opencollective.com/tauri). + +## License + +Code: (c) 2015 - Present - The Tauri Programme within The Commons Conservancy. + +MIT or MIT/Apache 2.0 where applicable. diff --git a/plugins/geolocation/SECURITY.md b/plugins/geolocation/SECURITY.md new file mode 100644 index 0000000..4f09bba --- /dev/null +++ b/plugins/geolocation/SECURITY.md @@ -0,0 +1,23 @@ +# Security Policy + +**Do not report security vulnerabilities through public GitHub issues.** + +**Please use the [Private Vulnerability Disclosure](https://docs.github.com/en/code-security/security-advisories/guidance-on-reporting-and-writing-information-about-vulnerabilities/privately-reporting-a-security-vulnerability#privately-reporting-a-security-vulnerability) feature of GitHub.** + +Include as much of the following information: + +- Type of issue (e.g. improper input parsing, privilege escalation, etc.) +- The location of the affected source code (tag/branch/commit or direct URL) +- Any special configuration required to reproduce the issue +- The distribution affected or used to help us with reproduction of the issue +- Step-by-step instructions to reproduce the issue +- Ideally a reproduction repository +- Impact of the issue, including how an attacker might exploit the issue + +We prefer to receive reports in English. + +## Contact + +Please disclose a vulnerability or security relevant issue here: [https://github.com/tauri-apps/plugins-workspace/security/advisories/new](https://github.com/tauri-apps/plugins-workspace/security/advisories/new). + +Alternatively, you can also contact us by email via [security@tauri.app](mailto:security@tauri.app). diff --git a/plugins/geolocation/android/.gitignore b/plugins/geolocation/android/.gitignore new file mode 100644 index 0000000..c0f21ec --- /dev/null +++ b/plugins/geolocation/android/.gitignore @@ -0,0 +1,2 @@ +/build +/.tauri diff --git a/plugins/geolocation/android/build.gradle.kts b/plugins/geolocation/android/build.gradle.kts new file mode 100644 index 0000000..022f11d --- /dev/null +++ b/plugins/geolocation/android/build.gradle.kts @@ -0,0 +1,36 @@ +plugins { + id("com.android.library") + id("org.jetbrains.kotlin.android") +} + +android { + namespace = "app.tauri.geolocation" + compileSdk = 36 + + defaultConfig { + minSdk = 24 + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + consumerProguardFiles("consumer-rules.pro") + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 + } + kotlinOptions { + jvmTarget = "1.8" + } +} + +dependencies { + + implementation("androidx.core:core-ktx:1.9.0") + implementation("androidx.appcompat:appcompat:1.6.0") + implementation("com.google.android.material:material:1.7.0") + implementation("com.google.android.gms:play-services-location:21.3.0") + testImplementation("junit:junit:4.13.2") + androidTestImplementation("androidx.test.ext:junit:1.1.5") + androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1") + implementation(project(":tauri-android")) +} diff --git a/plugins/geolocation/android/consumer-rules.pro b/plugins/geolocation/android/consumer-rules.pro new file mode 100644 index 0000000..481bb43 --- /dev/null +++ b/plugins/geolocation/android/consumer-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/plugins/geolocation/android/settings.gradle b/plugins/geolocation/android/settings.gradle new file mode 100644 index 0000000..14a752e --- /dev/null +++ b/plugins/geolocation/android/settings.gradle @@ -0,0 +1,2 @@ +include ':tauri-android' +project(':tauri-android').projectDir = new File('./.tauri/tauri-api') diff --git a/plugins/geolocation/android/src/androidTest/java/ExampleInstrumentedTest.kt b/plugins/geolocation/android/src/androidTest/java/ExampleInstrumentedTest.kt new file mode 100644 index 0000000..a301dc2 --- /dev/null +++ b/plugins/geolocation/android/src/androidTest/java/ExampleInstrumentedTest.kt @@ -0,0 +1,28 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +package app.tauri.geolocation + +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.ext.junit.runners.AndroidJUnit4 + +import org.junit.Test +import org.junit.runner.RunWith + +import org.junit.Assert.* + +/** + * Instrumented test, which will execute on an Android device. + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +@RunWith(AndroidJUnit4::class) +class ExampleInstrumentedTest { + @Test + fun useAppContext() { + // Context of the app under test. + val appContext = InstrumentationRegistry.getInstrumentation().targetContext + assertEquals("app.tauri.geolocation", appContext.packageName) + } +} diff --git a/plugins/geolocation/android/src/main/AndroidManifest.xml b/plugins/geolocation/android/src/main/AndroidManifest.xml new file mode 100644 index 0000000..a47edd1 --- /dev/null +++ b/plugins/geolocation/android/src/main/AndroidManifest.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/plugins/geolocation/android/src/main/java/Geolocation.kt b/plugins/geolocation/android/src/main/java/Geolocation.kt new file mode 100644 index 0000000..f93d230 --- /dev/null +++ b/plugins/geolocation/android/src/main/java/Geolocation.kt @@ -0,0 +1,144 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +package app.tauri.geolocation + +import android.annotation.SuppressLint +import android.content.Context +import android.location.Location +import android.location.LocationManager +import android.os.SystemClock +import androidx.core.location.LocationManagerCompat +import app.tauri.Logger +import com.google.android.gms.common.ConnectionResult +import com.google.android.gms.common.GoogleApiAvailability +import com.google.android.gms.location.FusedLocationProviderClient +import com.google.android.gms.location.LocationCallback +import com.google.android.gms.location.LocationRequest +import com.google.android.gms.location.LocationResult +import com.google.android.gms.location.LocationServices +import com.google.android.gms.location.Priority + + +public class Geolocation(private val context: Context) { + private var fusedLocationClient: FusedLocationProviderClient? = null + private var locationCallback: LocationCallback? = null + + + fun isLocationServicesEnabled(): Boolean { + val lm = context.getSystemService(Context.LOCATION_SERVICE) as LocationManager + return LocationManagerCompat.isLocationEnabled(lm) + } + + @SuppressWarnings("MissingPermission") + fun sendLocation(enableHighAccuracy: Boolean, successCallback: (location: Location) -> Unit, errorCallback: (error: String) -> Unit) { + val resultCode = GoogleApiAvailability.getInstance().isGooglePlayServicesAvailable(context); + if (resultCode == ConnectionResult.SUCCESS) { + val lm = context.getSystemService(Context.LOCATION_SERVICE) as LocationManager + + if (this.isLocationServicesEnabled()) { + var networkEnabled = false + + try { + networkEnabled = lm.isProviderEnabled(LocationManager.NETWORK_PROVIDER) + } catch (_: Exception) { + Logger.error("isProviderEnabled failed") + } + + val lowPrio = if (networkEnabled) Priority.PRIORITY_BALANCED_POWER_ACCURACY else Priority.PRIORITY_LOW_POWER + val prio = if (enableHighAccuracy) Priority.PRIORITY_HIGH_ACCURACY else lowPrio + + LocationServices + .getFusedLocationProviderClient(context) + .getCurrentLocation(prio, null) + .addOnFailureListener { e -> e.message?.let { errorCallback(it) } } + .addOnSuccessListener { location -> + if (location == null) { + errorCallback("Location unavailable.") + } else { + successCallback(location) + } + } + } else { + errorCallback("Location disabled.") + } + } else { + errorCallback("Google Play Services unavailable.") + } + } + + @SuppressLint("MissingPermission") + fun requestLocationUpdates(enableHighAccuracy: Boolean, timeout: Long, successCallback: (location: Location) -> Unit, errorCallback: (error: String) -> Unit) { + val resultCode = GoogleApiAvailability.getInstance().isGooglePlayServicesAvailable(context); + if (resultCode == ConnectionResult.SUCCESS) { + clearLocationUpdates() + fusedLocationClient = LocationServices.getFusedLocationProviderClient(context) + + val lm = context.getSystemService(Context.LOCATION_SERVICE) as LocationManager + + if (this.isLocationServicesEnabled()) { + var networkEnabled = false + + try { + networkEnabled = lm.isProviderEnabled(LocationManager.NETWORK_PROVIDER) + } catch (_: Exception) { + Logger.error("isProviderEnabled failed") + } + + val lowPrio = if (networkEnabled) Priority.PRIORITY_BALANCED_POWER_ACCURACY else Priority.PRIORITY_LOW_POWER + val prio = if (enableHighAccuracy) Priority.PRIORITY_HIGH_ACCURACY else lowPrio + + val locationRequest = LocationRequest.Builder(timeout) + .setMaxUpdateDelayMillis(timeout) + .setMinUpdateIntervalMillis(timeout) + .setPriority(prio) + .build() + + locationCallback = + object : LocationCallback() { + override fun onLocationResult(locationResult: LocationResult) { + val lastLocation = locationResult.lastLocation + if (lastLocation == null) { + errorCallback("Location unavailable.") + } else { + successCallback(lastLocation) + } + } + } + + fusedLocationClient?.requestLocationUpdates(locationRequest, locationCallback!!, null) + } else { + errorCallback("Location disabled.") + } + } else { + errorCallback("Google Play Services not available.") + } + } + + fun clearLocationUpdates() { + if (locationCallback != null) { + fusedLocationClient?.removeLocationUpdates(locationCallback!!) + locationCallback = null + } + } + + @SuppressLint("MissingPermission") + fun getLastLocation(maximumAge: Long): Location? { + var lastLoc: Location? = null + val lm = context.getSystemService(Context.LOCATION_SERVICE) as LocationManager + + for (provider in lm.allProviders) { + val tmpLoc = lm.getLastKnownLocation(provider) + if (tmpLoc != null) { + val locationAge = SystemClock.elapsedRealtimeNanos() - tmpLoc.elapsedRealtimeNanos + val maxAgeNano = maximumAge * 1000000L + if (locationAge <= maxAgeNano && (lastLoc == null || lastLoc.elapsedRealtimeNanos > tmpLoc.elapsedRealtimeNanos)) { + lastLoc = tmpLoc + } + } + } + + return lastLoc + } +} diff --git a/plugins/geolocation/android/src/main/java/GeolocationPlugin.kt b/plugins/geolocation/android/src/main/java/GeolocationPlugin.kt new file mode 100644 index 0000000..cf81f5e --- /dev/null +++ b/plugins/geolocation/android/src/main/java/GeolocationPlugin.kt @@ -0,0 +1,172 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +package app.tauri.geolocation + +import android.Manifest +import android.app.Activity +import android.location.Location +import android.os.Build +import android.webkit.WebView +import app.tauri.Logger +import app.tauri.PermissionState +import app.tauri.annotation.Command +import app.tauri.annotation.InvokeArg +import app.tauri.annotation.Permission +import app.tauri.annotation.PermissionCallback +import app.tauri.annotation.TauriPlugin +import app.tauri.plugin.Channel +import app.tauri.plugin.Invoke +import app.tauri.plugin.JSObject +import app.tauri.plugin.Plugin + +@InvokeArg +class PositionOptions { + var enableHighAccuracy: Boolean = false + var maximumAge: Long = 0 + var timeout: Long = 10000 +} + +@InvokeArg +class WatchArgs { + var options: PositionOptions = PositionOptions() + lateinit var channel: Channel +} + +@InvokeArg +class ClearWatchArgs { + var channelId: Long = 0 +} + +// TODO: Plugin does not ask user to enable google location services (like gmaps does) + +private const val ALIAS_LOCATION: String = "location" +private const val ALIAS_COARSE_LOCATION: String = "coarseLocation" + +@TauriPlugin( + permissions = [ + Permission(strings = [ + Manifest.permission.ACCESS_FINE_LOCATION, + Manifest.permission.ACCESS_COARSE_LOCATION + ], + alias = ALIAS_LOCATION + ), + Permission(strings = [ + Manifest.permission.ACCESS_COARSE_LOCATION + ], + alias = ALIAS_COARSE_LOCATION + ) + ] +) +class GeolocationPlugin(private val activity: Activity): Plugin(activity) { + private lateinit var implementation: Geolocation + private var watchers = hashMapOf>() + + override fun load(webView: WebView) { + super.load(webView) + implementation = Geolocation(activity.applicationContext) + } + + override fun onPause() { + super.onPause() + // Clear all location updates on pause to avoid possible background location calls + implementation.clearLocationUpdates() + } + + override fun onResume() { + super.onResume() + // resume watchers + for ((watcher, args) in watchers.values) { + startWatch(watcher, args) + } + } + + @Command + override fun checkPermissions(invoke: Invoke) { + if (implementation.isLocationServicesEnabled()) { + super.checkPermissions(invoke) + } else { + invoke.reject("Location services are disabled.") + } + } + + @Command + override fun requestPermissions(invoke: Invoke) { + if (implementation.isLocationServicesEnabled()) { + super.requestPermissions(invoke) + } else { + invoke.reject("Location services are disabled.") + } + } + + @Command + fun getCurrentPosition(invoke: Invoke) { + val args = invoke.parseArgs(PositionOptions::class.java) + + val location = implementation.getLastLocation(args.maximumAge) + if (location != null) { + invoke.resolve(convertLocation(location)) + } else { + implementation.sendLocation(args.enableHighAccuracy, + { loc -> invoke.resolve(convertLocation(loc)) }, + { error -> invoke.reject(error) }) + } + } + + @PermissionCallback + private fun positionPermissionCallback(invoke: Invoke) { + val permissionsResultJSON = JSObject() + permissionsResultJSON.put("location", getPermissionState(ALIAS_LOCATION)) + permissionsResultJSON.put("coarseLocation", getPermissionState(ALIAS_COARSE_LOCATION)) + invoke.resolve(permissionsResultJSON) + } + + @Command + fun watchPosition(invoke: Invoke) { + val args = invoke.parseArgs(WatchArgs::class.java) + startWatch(invoke, args) + } + + private fun startWatch(invoke: Invoke, args: WatchArgs) { + implementation.requestLocationUpdates( + args.options.enableHighAccuracy, + args.options.timeout, + { location -> args.channel.send(convertLocation(location)) }, + { error -> args.channel.sendObject(error) }) + + watchers[args.channel.id] = Pair(invoke, args) + } + + @Command + fun clearWatch(invoke: Invoke) { + val args = invoke.parseArgs(ClearWatchArgs::class.java) + + watchers.remove(args.channelId) + + if (watchers.isEmpty()) { + implementation.clearLocationUpdates() + } + + invoke.resolve() + } + + private fun convertLocation(location: Location): JSObject { + val ret = JSObject() + val coords = JSObject() + + coords.put("latitude", location.latitude) + coords.put("longitude", location.longitude) + coords.put("accuracy", location.accuracy) + coords.put("altitude", location.altitude) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + coords.put("altitudeAccuracy", location.verticalAccuracyMeters) + } + coords.put("speed", location.speed) + coords.put("heading", location.bearing) + ret.put("timestamp", location.time) + ret.put("coords", coords) + + return ret + } +} diff --git a/plugins/geolocation/android/src/test/java/ExampleUnitTest.kt b/plugins/geolocation/android/src/test/java/ExampleUnitTest.kt new file mode 100644 index 0000000..03c99f7 --- /dev/null +++ b/plugins/geolocation/android/src/test/java/ExampleUnitTest.kt @@ -0,0 +1,21 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +package app.tauri.geolocation + +import org.junit.Test + +import org.junit.Assert.* + +/** + * Example local unit test, which will execute on the development machine (host). + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +class ExampleUnitTest { + @Test + fun addition_isCorrect() { + assertEquals(4, 2 + 2) + } +} diff --git a/plugins/geolocation/api-iife.js b/plugins/geolocation/api-iife.js new file mode 100644 index 0000000..ef93840 --- /dev/null +++ b/plugins/geolocation/api-iife.js @@ -0,0 +1 @@ +if("__TAURI__"in window){var __TAURI_PLUGIN_GEOLOCATION__=function(n,i){"use strict";return n.checkPermissions=async function(){return await i.checkPermissions("geolocation")},n.clearWatch=async function(n){await i.invoke("plugin:geolocation|clear_watch",{channelId:n})},n.getCurrentPosition=async function(n){return await i.invoke("plugin:geolocation|get_current_position",{options:n})},n.requestPermissions=async function(n){return await i.invoke("plugin:geolocation|request_permissions",{permissions:n})},n.watchPosition=async function(n,o){const e=new i.Channel;return e.onmessage=n=>{"string"==typeof n?o(null,n):o(n)},await i.invoke("plugin:geolocation|watch_position",{options:n,channel:e}),e.id},n}({},window.__TAURI__.core);Object.defineProperty(window.__TAURI__,"geolocation",{value:__TAURI_PLUGIN_GEOLOCATION__})} diff --git a/plugins/geolocation/build.rs b/plugins/geolocation/build.rs new file mode 100644 index 0000000..b524976 --- /dev/null +++ b/plugins/geolocation/build.rs @@ -0,0 +1,24 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +const COMMANDS: &[&str] = &[ + "get_current_position", + "watch_position", + "clear_watch", + "check_permissions", + "request_permissions", +]; + +fn main() { + let result = tauri_plugin::Builder::new(COMMANDS) + .global_api_script_path("./api-iife.js") + .android_path("android") + .ios_path("ios") + .try_build(); + + // when building documentation for Android the plugin build result is always Err() and is irrelevant to the crate documentation build + if !(cfg!(docsrs) && std::env::var("TARGET").unwrap().contains("android")) { + result.unwrap(); + } +} diff --git a/plugins/geolocation/contributors/crabnebula.svg b/plugins/geolocation/contributors/crabnebula.svg new file mode 100644 index 0000000..40e2413 --- /dev/null +++ b/plugins/geolocation/contributors/crabnebula.svg @@ -0,0 +1,31 @@ + \ No newline at end of file diff --git a/plugins/geolocation/contributors/rescue.png b/plugins/geolocation/contributors/rescue.png new file mode 100644 index 0000000000000000000000000000000000000000..2b5916f408daacf0ae1938d2e9dc7511ceffdbbf GIT binary patch literal 8071 zcmZ8`by$?$7cHIA0@B?KAt6Xf_keT`ozguZAT3=2qI5`i!_W=VC5|Rym9w_|qsK7HX?^PaSpwI*0Zyvs?|Bd0R$OB6a9tb@K zkF5V-4<)$Tqt0Lbza=%eCR`By2tTmktD4Zgy2!oKqlbs>zh}V>Jis0;!IKYWgzja; z|26ZkD_OCByukxC(FZzQ@V^uP9syrf;5v^!|F@S1OG5XuqW{?VyM6uGmmEC#X!39J zAFGd+;7%0&9n=Hn5hnl0g2Uhq!lwsk$^TUldhnzAFAN?$#PHr9%{<8d-Tn<805%V_*H~b*@IC&+JhXFpY z{>SIvLm%e$pXW#X|7Yf}$iuJpZ}o2mzWvX}L+M|8@EGp!^^5;$>XE4OKY;(seLVVM z`=9v#3wuz8SN~P}yM4$A-NOap>jUvGFpaB92y=So0y!Qott0ywz#ysvbwgm zw!X3PeQOK4y|cG}aCmrpeDd=Y_Ur8Y;_COG>zmuVJL~A!Qv?KxSBf%{T0Tp&#&)lW z2UDFe$X}~VKI!)F7dTG*DRbQWECs(wVZin|7c1Foxk!tZv})^3LK*7j&FA(#&(2CD z31a9GPwA1O(82}@jb5o^Ac0*dd~lmxPL9Oqw2kLhR(=Oq=C+NC8K2L29b{-ylKH9_ z9nwJUMhWs%Y0?TM(Kj{fo>L@JwoQ0fHMLA~m$1ZOTi~evU)h9D6I^U%wUY^`g zXQ^Di$al9gs#iEJWJ^9OvCreQ8ju()HkaI3;M_n@8uikhK=`9jP{^+>g8>ome

fjuOLaq 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 literal 0 HcmV?d00001 diff --git a/plugins/geolocation/guest-js/index.ts b/plugins/geolocation/guest-js/index.ts new file mode 100644 index 0000000..2c05bda --- /dev/null +++ b/plugins/geolocation/guest-js/index.ts @@ -0,0 +1,239 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +/** + * Get and track the device's current position, mirroring the W3C Geolocation API. + * + * @module + */ + +import { + Channel, + invoke, + PermissionState, + checkPermissions as checkPluginPermissions +} from '@tauri-apps/api/core' + +/** + * The GPS coordinates of a {@link Position}, along with the accuracy of each reading. + */ +export type Coordinates = { + /** + * Latitude in decimal degrees. + */ + latitude: number + /** + * Longitude in decimal degrees. + */ + longitude: number + /** + * Accuracy level of the latitude and longitude coordinates in meters. + */ + accuracy: number + /** + * Accuracy level of the altitude coordinate in meters, if available. + * Available on all iOS versions and on Android 8 and above. + */ + altitudeAccuracy: number | null + /** + * The altitude the user is at, if available. + */ + altitude: number | null + /** + * The speed the user is traveling, in meters per second, if available. + */ + speed: number | null + /** + * The heading the user is facing, if available. + */ + heading: number | null +} + +/** + * The current permission state for the geolocation APIs. + */ +export type PermissionStatus = { + /** + * Permission state for the location alias. + * + * On Android it requests/checks both ACCESS_COARSE_LOCATION and ACCESS_FINE_LOCATION permissions. + * + * On iOS it requests/checks location permissions. + */ + location: PermissionState + /** + * Permissions state for the coarseLoaction alias. + * + * On Android it requests/checks ACCESS_COARSE_LOCATION. + * + * On Android 12+, users can choose between Approximate location (ACCESS_COARSE_LOCATION) and Precise location (ACCESS_FINE_LOCATION). + * + * On iOS it will have the same value as the `location` alias. + */ + coarseLocation: PermissionState +} + +/** + * The individual permission aliases that can be requested with {@link requestPermissions}. + * + * `location` maps to both the coarse and fine location permissions on Android and to the standard + * location permission on iOS. `coarseLocation` maps to the coarse location permission only on + * Android, and behaves the same as `location` on iOS. + */ +export type PermissionType = 'location' | 'coarseLocation' + +/** + * A geolocation reading, as returned by {@link getCurrentPosition} and passed to the callback of {@link watchPosition}. + */ +export type Position = { + /** + * Creation time for these coordinates, in milliseconds since the Unix epoch. + */ + timestamp: number + /** + * The GPS coordinates along with the accuracy of the data. + */ + coords: Coordinates +} + +/** + * Options used to configure a {@link getCurrentPosition} or {@link watchPosition} request. + */ +export type PositionOptions = { + /** + * High accuracy mode (such as GPS, if available) + * Will be ignored on Android 12+ if users didn't grant the ACCESS_FINE_LOCATION permission (`coarseLocation` permission). + */ + enableHighAccuracy: boolean + /** + * The maximum wait time in milliseconds for location updates. + * On Android the timeout gets ignored for getCurrentPosition. + * Ignored on iOS + */ + timeout: number + /** + * The maximum age in milliseconds of a possible cached position that is acceptable to return. + * Default: 0 + * Ignored on iOS + */ + maximumAge: number +} + +/** + * Registers a callback that is invoked with the device's position whenever it changes, similar to the W3C `navigator.geolocation.watchPosition` API. Pass the returned id to {@link clearWatch} to stop watching. + * + * @example + * ```typescript + * import { watchPosition } from '@tauri-apps/plugin-geolocation'; + * const watchId = await watchPosition( + * { enableHighAccuracy: true, timeout: 10000, maximumAge: 0 }, + * (position, error) => { + * if (error) { + * console.error(error) + * } else { + * console.log(position) + * } + * } + * ); + * ``` + * + * @param options Configuration for the position watcher. + * @param cb Callback invoked with the new {@link Position} on success, or `null` and an error message when a read fails. + * @returns A promise resolving to the id of the registered watcher. + * @since 2.0.0 + */ +export async function watchPosition( + options: PositionOptions, + cb: (location: Position | null, error?: string) => void +): Promise { + const channel = new Channel() + channel.onmessage = (message) => { + if (typeof message === 'string') { + cb(null, message) + } else { + cb(message) + } + } + await invoke('plugin:geolocation|watch_position', { + options, + channel + }) + return channel.id +} + +/** + * Returns the device's current position, similar to the W3C `navigator.geolocation.getCurrentPosition` API. + * + * @example + * ```typescript + * import { getCurrentPosition } from '@tauri-apps/plugin-geolocation'; + * const position = await getCurrentPosition(); + * ``` + * + * @param options Configuration for the position request. + * @returns A promise resolving to the current {@link Position}. + * @since 2.0.0 + */ +export async function getCurrentPosition( + options?: PositionOptions +): Promise { + return await invoke('plugin:geolocation|get_current_position', { + options + }) +} + +/** + * Stops the position watcher registered with {@link watchPosition}. + * + * @example + * ```typescript + * import { clearWatch } from '@tauri-apps/plugin-geolocation'; + * await clearWatch(watchId); + * ``` + * + * @param channelId The id returned by {@link watchPosition}. + * @since 2.0.0 + */ +export async function clearWatch(channelId: number): Promise { + await invoke('plugin:geolocation|clear_watch', { + channelId + }) +} + +/** + * Returns the current state of the geolocation permissions. Rejects if location services are disabled on the device. + * + * @example + * ```typescript + * import { checkPermissions } from '@tauri-apps/plugin-geolocation'; + * const permission = await checkPermissions(); + * ``` + * + * @returns A promise resolving to the current {@link PermissionStatus}. + * @since 2.0.0 + */ +export async function checkPermissions(): Promise { + return await checkPluginPermissions('geolocation') +} + +/** + * Requests the given geolocation permissions, prompting the user if needed. Rejects if location services are disabled on the device. + * + * @example + * ```typescript + * import { requestPermissions } from '@tauri-apps/plugin-geolocation'; + * const permission = await requestPermissions(['location']); + * ``` + * + * @param permissions The permissions to request, or `null` to request all of them. + * @returns A promise resolving to the resulting {@link PermissionStatus}. + * @since 2.0.0 + */ +export async function requestPermissions( + permissions: PermissionType[] | null +): Promise { + return await invoke('plugin:geolocation|request_permissions', { + permissions + }) +} diff --git a/plugins/geolocation/ios/.gitignore b/plugins/geolocation/ios/.gitignore new file mode 100644 index 0000000..5922fda --- /dev/null +++ b/plugins/geolocation/ios/.gitignore @@ -0,0 +1,10 @@ +.DS_Store +/.build +/Packages +/*.xcodeproj +xcuserdata/ +DerivedData/ +.swiftpm/config/registries.json +.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata +.netrc +Package.resolved diff --git a/plugins/geolocation/ios/Package.swift b/plugins/geolocation/ios/Package.swift new file mode 100644 index 0000000..f8cfb73 --- /dev/null +++ b/plugins/geolocation/ios/Package.swift @@ -0,0 +1,34 @@ +// swift-tools-version:5.3 +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +import PackageDescription + +let package = Package( + name: "tauri-plugin-geolocation", + platforms: [ + .macOS(.v10_13), + .iOS(.v13), + ], + products: [ + // Products define the executables and libraries a package produces, and make them visible to other packages. + .library( + name: "tauri-plugin-geolocation", + type: .static, + targets: ["tauri-plugin-geolocation"]) + ], + dependencies: [ + .package(name: "Tauri", path: "../.tauri/tauri-api") + ], + targets: [ + // Targets are the basic building blocks of a package. A target can define a module or a test suite. + // Targets can depend on other targets in this package, and on products in packages this package depends on. + .target( + name: "tauri-plugin-geolocation", + dependencies: [ + .byName(name: "Tauri") + ], + path: "Sources") + ] +) diff --git a/plugins/geolocation/ios/README.md b/plugins/geolocation/ios/README.md new file mode 100644 index 0000000..5612ac8 --- /dev/null +++ b/plugins/geolocation/ios/README.md @@ -0,0 +1,3 @@ +# Tauri Plugin Geolocation + +A description of this package. diff --git a/plugins/geolocation/ios/Sources/GeolocationPlugin.swift b/plugins/geolocation/ios/Sources/GeolocationPlugin.swift new file mode 100644 index 0000000..7a2b57a --- /dev/null +++ b/plugins/geolocation/ios/Sources/GeolocationPlugin.swift @@ -0,0 +1,250 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +import CoreLocation +import SwiftRs +import Tauri +import UIKit +import WebKit + +class GetPositionArgs: Decodable { + var enableHighAccuracy: Bool? +} + +class WatchPositionArgs: Decodable { + let options: GetPositionArgs + let channel: Channel +} + +class ClearWatchArgs: Decodable { + let channelId: UInt32 +} + +class GeolocationPlugin: Plugin, CLLocationManagerDelegate { + private let locationManager = CLLocationManager() + private var isUpdatingLocation: Bool = false + private var permissionRequests: [Invoke] = [] + private var positionRequests: [Invoke] = [] + private var watcherChannels: [Channel] = [] + + override init() { + super.init() + locationManager.delegate = self + } + + // + // Tauri commands + // + + @objc public func getCurrentPosition(_ invoke: Invoke) throws { + let args = try invoke.parseArgs(GetPositionArgs.self) + + self.positionRequests.append(invoke) + + DispatchQueue.main.async { + if args.enableHighAccuracy == true { + self.locationManager.desiredAccuracy = kCLLocationAccuracyBest + } else { + self.locationManager.desiredAccuracy = kCLLocationAccuracyKilometer + } + + // TODO: Use the authorizationStatus instance property with locationManagerDidChangeAuthorization(_:) instead. + if CLLocationManager.authorizationStatus() == .notDetermined { + self.locationManager.requestWhenInUseAuthorization() + } else { + self.locationManager.requestLocation() + } + } + } + + @objc public func watchPosition(_ invoke: Invoke) throws { + let args = try invoke.parseArgs(WatchPositionArgs.self) + + self.watcherChannels.append(args.channel) + + DispatchQueue.main.async { + if args.options.enableHighAccuracy == true { + self.locationManager.desiredAccuracy = kCLLocationAccuracyBest + } else { + self.locationManager.desiredAccuracy = kCLLocationAccuracyKilometer + } + + // TODO: Use the authorizationStatus instance property with locationManagerDidChangeAuthorization(_:) instead. + if CLLocationManager.authorizationStatus() == .notDetermined { + self.locationManager.requestWhenInUseAuthorization() + } else { + self.locationManager.startUpdatingLocation() + self.isUpdatingLocation = true + } + } + + invoke.resolve() + } + + @objc public func clearWatch(_ invoke: Invoke) throws { + let args = try invoke.parseArgs(ClearWatchArgs.self) + + self.watcherChannels = self.watcherChannels.filter { $0.id != args.channelId } + + // TODO: capacitor plugin calls stopUpdating unconditionally + if self.watcherChannels.isEmpty { + self.stopUpdating() + } + + invoke.resolve() + } + + @objc override public func checkPermissions(_ invoke: Invoke) { + var status: String = "" + + if CLLocationManager.locationServicesEnabled() { + // TODO: Use the authorizationStatus instance property with locationManagerDidChangeAuthorization(_:) instead. + switch CLLocationManager.authorizationStatus() { + case .notDetermined: + status = "prompt" + case .restricted, .denied: + status = "denied" + case .authorizedAlways, .authorizedWhenInUse: + status = "granted" + @unknown default: + status = "prompt" + } + } else { + invoke.reject("Location services are not enabled.") + return + } + + let result = ["location": status, "coarseLocation": status] + + invoke.resolve(result) + } + + @objc override public func requestPermissions(_ invoke: Invoke) { + if CLLocationManager.locationServicesEnabled() { + // TODO: Use the authorizationStatus instance property with locationManagerDidChangeAuthorization(_:) instead. + if CLLocationManager.authorizationStatus() == .notDetermined { + self.permissionRequests.append(invoke) + + DispatchQueue.main.async { + self.locationManager.requestWhenInUseAuthorization() + } + } else { + checkPermissions(invoke) + } + } else { + invoke.reject("Location services are not enabled.") + } + } + + // + // Delegate methods + // + + public func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) { + Logger.error(error) + + let requests = self.positionRequests + self.permissionRequests + self.positionRequests.removeAll() + self.permissionRequests.removeAll() + + for request in requests { + request.reject(error.localizedDescription) + } + + for channel in self.watcherChannels { + do { + try channel.send(error.localizedDescription) + } catch { + Logger.error(error) + } + } + } + + public func locationManager( + _ manager: CLLocationManager, didUpdateLocations locations: [CLLocation] + ) { + // Respond to all getCurrentPosition() calls. + for request in self.positionRequests { + // The capacitor plugin uses locations.first but .last should be the most current one + // and i don't see a reason to use old locations + if let location = locations.last { + let result = convertLocation(location) + request.resolve(result) + } else { + request.reject("Location service returned an empty Location array.") + } + } + + for channel in self.watcherChannels { + // The capacitor plugin uses locations.first but .last should be the most recent one + // and i don't see a reason to use old locations + if let location = locations.last { + let result = convertLocation(location) + do { + try channel.send(result) + } catch { + Logger.error(error) + } + } else { + do { + try channel.send("Location service returned an empty Location array.") + } catch { + Logger.error(error) + } + } + } + } + + public func locationManager( + _ manager: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus + ) { + let requests = self.permissionRequests + self.permissionRequests.removeAll() + + for request in requests { + checkPermissions(request) + } + + if !self.positionRequests.isEmpty { + self.locationManager.requestLocation() + } + + if !self.watcherChannels.isEmpty && !self.isUpdatingLocation { + self.locationManager.startUpdatingLocation() + self.isUpdatingLocation = true + } + } + + // + // Internal/Helper methods + // + + // TODO: Why is this pub in capacitor + private func stopUpdating() { + self.locationManager.stopUpdatingLocation() + self.isUpdatingLocation = false + } + + private func convertLocation(_ location: CLLocation) -> JsonObject { + var ret: JsonObject = [:] + var coords: JsonObject = [:] + + coords["latitude"] = location.coordinate.latitude + coords["longitude"] = location.coordinate.longitude + coords["accuracy"] = location.horizontalAccuracy + coords["altitude"] = location.altitude + coords["altitudeAccuracy"] = location.verticalAccuracy + coords["speed"] = location.speed + coords["heading"] = location.course + ret["timestamp"] = Int((location.timestamp.timeIntervalSince1970 * 1000)) + ret["coords"] = coords + + return ret + } +} + +@_cdecl("init_plugin_geolocation") +func initPlugin() -> Plugin { + return GeolocationPlugin() +} diff --git a/plugins/geolocation/ios/Tests/PluginTests/PluginTests.swift b/plugins/geolocation/ios/Tests/PluginTests/PluginTests.swift new file mode 100644 index 0000000..99992ce --- /dev/null +++ b/plugins/geolocation/ios/Tests/PluginTests/PluginTests.swift @@ -0,0 +1,12 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +import XCTest +@testable import ExamplePlugin + +final class ExamplePluginTests: XCTestCase { + func testExample() throws { + let plugin = ExamplePlugin() + } +} diff --git a/plugins/geolocation/package.json b/plugins/geolocation/package.json new file mode 100644 index 0000000..32bbb0e --- /dev/null +++ b/plugins/geolocation/package.json @@ -0,0 +1,29 @@ +{ + "name": "@tauri-apps/plugin-geolocation", + "version": "2.3.3", + "license": "MIT OR Apache-2.0", + "authors": [ + "Tauri Programme within The Commons Conservancy" + ], + "repository": "https://github.com/tauri-apps/plugins-workspace", + "type": "module", + "types": "./dist-js/index.d.ts", + "main": "./dist-js/index.cjs", + "module": "./dist-js/index.js", + "exports": { + "types": "./dist-js/index.d.ts", + "import": "./dist-js/index.js", + "require": "./dist-js/index.cjs" + }, + "scripts": { + "build": "rollup -c" + }, + "files": [ + "dist-js", + "README.md", + "LICENSE" + ], + "dependencies": { + "@tauri-apps/api": "^2.11.0" + } +} diff --git a/plugins/geolocation/permissions/autogenerated/commands/check_permissions.toml b/plugins/geolocation/permissions/autogenerated/commands/check_permissions.toml new file mode 100644 index 0000000..f5af08b --- /dev/null +++ b/plugins/geolocation/permissions/autogenerated/commands/check_permissions.toml @@ -0,0 +1,13 @@ +# Automatically generated - DO NOT EDIT! + +"$schema" = "../../schemas/schema.json" + +[[permission]] +identifier = "allow-check-permissions" +description = "Enables the check_permissions command without any pre-configured scope." +commands.allow = ["check_permissions"] + +[[permission]] +identifier = "deny-check-permissions" +description = "Denies the check_permissions command without any pre-configured scope." +commands.deny = ["check_permissions"] diff --git a/plugins/geolocation/permissions/autogenerated/commands/clear_permissions.toml b/plugins/geolocation/permissions/autogenerated/commands/clear_permissions.toml new file mode 100644 index 0000000..a3e6ab5 --- /dev/null +++ b/plugins/geolocation/permissions/autogenerated/commands/clear_permissions.toml @@ -0,0 +1,13 @@ +# Automatically generated - DO NOT EDIT! + +"$schema" = "../../schemas/schema.json" + +[[permission]] +identifier = "allow-clear-permissions" +description = "Enables the clear_permissions command without any pre-configured scope." +commands.allow = ["clear_permissions"] + +[[permission]] +identifier = "deny-clear-permissions" +description = "Denies the clear_permissions command without any pre-configured scope." +commands.deny = ["clear_permissions"] diff --git a/plugins/geolocation/permissions/autogenerated/commands/clear_watch.toml b/plugins/geolocation/permissions/autogenerated/commands/clear_watch.toml new file mode 100644 index 0000000..19fb577 --- /dev/null +++ b/plugins/geolocation/permissions/autogenerated/commands/clear_watch.toml @@ -0,0 +1,13 @@ +# Automatically generated - DO NOT EDIT! + +"$schema" = "../../schemas/schema.json" + +[[permission]] +identifier = "allow-clear-watch" +description = "Enables the clear_watch command without any pre-configured scope." +commands.allow = ["clear_watch"] + +[[permission]] +identifier = "deny-clear-watch" +description = "Denies the clear_watch command without any pre-configured scope." +commands.deny = ["clear_watch"] diff --git a/plugins/geolocation/permissions/autogenerated/commands/get_current_position.toml b/plugins/geolocation/permissions/autogenerated/commands/get_current_position.toml new file mode 100644 index 0000000..fefb951 --- /dev/null +++ b/plugins/geolocation/permissions/autogenerated/commands/get_current_position.toml @@ -0,0 +1,13 @@ +# Automatically generated - DO NOT EDIT! + +"$schema" = "../../schemas/schema.json" + +[[permission]] +identifier = "allow-get-current-position" +description = "Enables the get_current_position command without any pre-configured scope." +commands.allow = ["get_current_position"] + +[[permission]] +identifier = "deny-get-current-position" +description = "Denies the get_current_position command without any pre-configured scope." +commands.deny = ["get_current_position"] diff --git a/plugins/geolocation/permissions/autogenerated/commands/request_permissions.toml b/plugins/geolocation/permissions/autogenerated/commands/request_permissions.toml new file mode 100644 index 0000000..02dcd62 --- /dev/null +++ b/plugins/geolocation/permissions/autogenerated/commands/request_permissions.toml @@ -0,0 +1,13 @@ +# Automatically generated - DO NOT EDIT! + +"$schema" = "../../schemas/schema.json" + +[[permission]] +identifier = "allow-request-permissions" +description = "Enables the request_permissions command without any pre-configured scope." +commands.allow = ["request_permissions"] + +[[permission]] +identifier = "deny-request-permissions" +description = "Denies the request_permissions command without any pre-configured scope." +commands.deny = ["request_permissions"] diff --git a/plugins/geolocation/permissions/autogenerated/commands/watch_position.toml b/plugins/geolocation/permissions/autogenerated/commands/watch_position.toml new file mode 100644 index 0000000..5878ba1 --- /dev/null +++ b/plugins/geolocation/permissions/autogenerated/commands/watch_position.toml @@ -0,0 +1,13 @@ +# Automatically generated - DO NOT EDIT! + +"$schema" = "../../schemas/schema.json" + +[[permission]] +identifier = "allow-watch-position" +description = "Enables the watch_position command without any pre-configured scope." +commands.allow = ["watch_position"] + +[[permission]] +identifier = "deny-watch-position" +description = "Denies the watch_position command without any pre-configured scope." +commands.deny = ["watch_position"] diff --git a/plugins/geolocation/permissions/autogenerated/reference.md b/plugins/geolocation/permissions/autogenerated/reference.md new file mode 100644 index 0000000..0215a99 --- /dev/null +++ b/plugins/geolocation/permissions/autogenerated/reference.md @@ -0,0 +1,165 @@ +## Permission Table + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
IdentifierDescription
+ +`geolocation:allow-check-permissions` + + + +Enables the check_permissions command without any pre-configured scope. + +
+ +`geolocation:deny-check-permissions` + + + +Denies the check_permissions command without any pre-configured scope. + +
+ +`geolocation:allow-clear-permissions` + + + +Enables the clear_permissions command without any pre-configured scope. + +
+ +`geolocation:deny-clear-permissions` + + + +Denies the clear_permissions command without any pre-configured scope. + +
+ +`geolocation:allow-clear-watch` + + + +Enables the clear_watch command without any pre-configured scope. + +
+ +`geolocation:deny-clear-watch` + + + +Denies the clear_watch command without any pre-configured scope. + +
+ +`geolocation:allow-get-current-position` + + + +Enables the get_current_position command without any pre-configured scope. + +
+ +`geolocation:deny-get-current-position` + + + +Denies the get_current_position command without any pre-configured scope. + +
+ +`geolocation:allow-request-permissions` + + + +Enables the request_permissions command without any pre-configured scope. + +
+ +`geolocation:deny-request-permissions` + + + +Denies the request_permissions command without any pre-configured scope. + +
+ +`geolocation:allow-watch-position` + + + +Enables the watch_position command without any pre-configured scope. + +
+ +`geolocation:deny-watch-position` + + + +Denies the watch_position command without any pre-configured scope. + +
diff --git a/plugins/geolocation/permissions/schemas/schema.json b/plugins/geolocation/permissions/schemas/schema.json new file mode 100644 index 0000000..237b445 --- /dev/null +++ b/plugins/geolocation/permissions/schemas/schema.json @@ -0,0 +1,372 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "PermissionFile", + "description": "Permission file that can define a default permission, a set of permissions or a list of inlined permissions.", + "type": "object", + "properties": { + "default": { + "description": "The default permission set for the plugin", + "anyOf": [ + { + "$ref": "#/definitions/DefaultPermission" + }, + { + "type": "null" + } + ] + }, + "set": { + "description": "A list of permissions sets defined", + "type": "array", + "items": { + "$ref": "#/definitions/PermissionSet" + } + }, + "permission": { + "description": "A list of inlined permissions", + "default": [], + "type": "array", + "items": { + "$ref": "#/definitions/Permission" + } + } + }, + "definitions": { + "DefaultPermission": { + "description": "The default permission set of the plugin.\n\nWorks similarly to a permission with the \"default\" identifier.", + "type": "object", + "required": [ + "permissions" + ], + "properties": { + "version": { + "description": "The version of the permission.", + "type": [ + "integer", + "null" + ], + "format": "uint64", + "minimum": 1.0 + }, + "description": { + "description": "Human-readable description of what the permission does. Tauri convention is to use `

` headings in markdown content for Tauri documentation generation purposes.", + "type": [ + "string", + "null" + ] + }, + "permissions": { + "description": "All permissions this set contains.", + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "PermissionSet": { + "description": "A set of direct permissions grouped together under a new name.", + "type": "object", + "required": [ + "description", + "identifier", + "permissions" + ], + "properties": { + "identifier": { + "description": "A unique identifier for the permission.", + "type": "string" + }, + "description": { + "description": "Human-readable description of what the permission does.", + "type": "string" + }, + "permissions": { + "description": "All permissions this set contains.", + "type": "array", + "items": { + "$ref": "#/definitions/PermissionKind" + } + } + } + }, + "Permission": { + "description": "Descriptions of explicit privileges of commands.\n\nIt can enable commands to be accessible in the frontend of the application.\n\nIf the scope is defined it can be used to fine grain control the access of individual or multiple commands.", + "type": "object", + "required": [ + "identifier" + ], + "properties": { + "version": { + "description": "The version of the permission.", + "type": [ + "integer", + "null" + ], + "format": "uint64", + "minimum": 1.0 + }, + "identifier": { + "description": "A unique identifier for the permission.", + "type": "string" + }, + "description": { + "description": "Human-readable description of what the permission does. Tauri internal convention is to use `

` headings in markdown content for Tauri documentation generation purposes.", + "type": [ + "string", + "null" + ] + }, + "commands": { + "description": "Allowed or denied commands when using this permission.", + "default": { + "allow": [], + "deny": [] + }, + "allOf": [ + { + "$ref": "#/definitions/Commands" + } + ] + }, + "scope": { + "description": "Allowed or denied scoped when using this permission.", + "allOf": [ + { + "$ref": "#/definitions/Scopes" + } + ] + }, + "platforms": { + "description": "Target platforms this permission applies. By default all platforms are affected by this permission.", + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/Target" + } + } + } + }, + "Commands": { + "description": "Allowed and denied commands inside a permission.\n\nIf two commands clash inside of `allow` and `deny`, it should be denied by default.", + "type": "object", + "properties": { + "allow": { + "description": "Allowed command.", + "default": [], + "type": "array", + "items": { + "type": "string" + } + }, + "deny": { + "description": "Denied command, which takes priority.", + "default": [], + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "Scopes": { + "description": "An argument for fine grained behavior control of Tauri commands.\n\nIt can be of any serde serializable type and is used to allow or prevent certain actions inside a Tauri command. The configured scope is passed to the command and will be enforced by the command implementation.\n\n## Example\n\n```json { \"allow\": [{ \"path\": \"$HOME/**\" }], \"deny\": [{ \"path\": \"$HOME/secret.txt\" }] } ```", + "type": "object", + "properties": { + "allow": { + "description": "Data that defines what is allowed by the scope.", + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/Value" + } + }, + "deny": { + "description": "Data that defines what is denied by the scope. This should be prioritized by validation logic.", + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/Value" + } + } + } + }, + "Value": { + "description": "All supported ACL values.", + "anyOf": [ + { + "description": "Represents a null JSON value.", + "type": "null" + }, + { + "description": "Represents a [`bool`].", + "type": "boolean" + }, + { + "description": "Represents a valid ACL [`Number`].", + "allOf": [ + { + "$ref": "#/definitions/Number" + } + ] + }, + { + "description": "Represents a [`String`].", + "type": "string" + }, + { + "description": "Represents a list of other [`Value`]s.", + "type": "array", + "items": { + "$ref": "#/definitions/Value" + } + }, + { + "description": "Represents a map of [`String`] keys to [`Value`]s.", + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/Value" + } + } + ] + }, + "Number": { + "description": "A valid ACL number.", + "anyOf": [ + { + "description": "Represents an [`i64`].", + "type": "integer", + "format": "int64" + }, + { + "description": "Represents a [`f64`].", + "type": "number", + "format": "double" + } + ] + }, + "Target": { + "description": "Platform target.", + "oneOf": [ + { + "description": "MacOS.", + "type": "string", + "enum": [ + "macOS" + ] + }, + { + "description": "Windows.", + "type": "string", + "enum": [ + "windows" + ] + }, + { + "description": "Linux.", + "type": "string", + "enum": [ + "linux" + ] + }, + { + "description": "Android.", + "type": "string", + "enum": [ + "android" + ] + }, + { + "description": "iOS.", + "type": "string", + "enum": [ + "iOS" + ] + } + ] + }, + "PermissionKind": { + "type": "string", + "oneOf": [ + { + "description": "Enables the check_permissions command without any pre-configured scope.", + "type": "string", + "const": "allow-check-permissions", + "markdownDescription": "Enables the check_permissions command without any pre-configured scope." + }, + { + "description": "Denies the check_permissions command without any pre-configured scope.", + "type": "string", + "const": "deny-check-permissions", + "markdownDescription": "Denies the check_permissions command without any pre-configured scope." + }, + { + "description": "Enables the clear_permissions command without any pre-configured scope.", + "type": "string", + "const": "allow-clear-permissions", + "markdownDescription": "Enables the clear_permissions command without any pre-configured scope." + }, + { + "description": "Denies the clear_permissions command without any pre-configured scope.", + "type": "string", + "const": "deny-clear-permissions", + "markdownDescription": "Denies the clear_permissions command without any pre-configured scope." + }, + { + "description": "Enables the clear_watch command without any pre-configured scope.", + "type": "string", + "const": "allow-clear-watch", + "markdownDescription": "Enables the clear_watch command without any pre-configured scope." + }, + { + "description": "Denies the clear_watch command without any pre-configured scope.", + "type": "string", + "const": "deny-clear-watch", + "markdownDescription": "Denies the clear_watch command without any pre-configured scope." + }, + { + "description": "Enables the get_current_position command without any pre-configured scope.", + "type": "string", + "const": "allow-get-current-position", + "markdownDescription": "Enables the get_current_position command without any pre-configured scope." + }, + { + "description": "Denies the get_current_position command without any pre-configured scope.", + "type": "string", + "const": "deny-get-current-position", + "markdownDescription": "Denies the get_current_position command without any pre-configured scope." + }, + { + "description": "Enables the request_permissions command without any pre-configured scope.", + "type": "string", + "const": "allow-request-permissions", + "markdownDescription": "Enables the request_permissions command without any pre-configured scope." + }, + { + "description": "Denies the request_permissions command without any pre-configured scope.", + "type": "string", + "const": "deny-request-permissions", + "markdownDescription": "Denies the request_permissions command without any pre-configured scope." + }, + { + "description": "Enables the watch_position command without any pre-configured scope.", + "type": "string", + "const": "allow-watch-position", + "markdownDescription": "Enables the watch_position command without any pre-configured scope." + }, + { + "description": "Denies the watch_position command without any pre-configured scope.", + "type": "string", + "const": "deny-watch-position", + "markdownDescription": "Denies the watch_position command without any pre-configured scope." + } + ] + } + } +} \ No newline at end of file diff --git a/plugins/geolocation/rollup.config.js b/plugins/geolocation/rollup.config.js new file mode 100644 index 0000000..1f349ec --- /dev/null +++ b/plugins/geolocation/rollup.config.js @@ -0,0 +1,7 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +import { createConfig } from '../../shared/rollup.config.js' + +export default createConfig() diff --git a/plugins/geolocation/src/commands.rs b/plugins/geolocation/src/commands.rs new file mode 100644 index 0000000..d2cae84 --- /dev/null +++ b/plugins/geolocation/src/commands.rs @@ -0,0 +1,42 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use tauri::{command, ipc::Channel, AppHandle, Runtime}; + +use crate::{GeolocationExt, PermissionStatus, PermissionType, Position, PositionOptions, Result}; + +#[command] +pub(crate) async fn get_current_position( + app: AppHandle, + options: Option, +) -> Result { + app.geolocation().get_current_position(options) +} + +#[command] +pub(crate) async fn watch_position( + app: AppHandle, + options: PositionOptions, + channel: Channel, +) -> Result<()> { + app.geolocation().watch_position_inner(options, channel) +} + +#[command] +pub(crate) async fn clear_watch(app: AppHandle, channel_id: u32) -> Result<()> { + app.geolocation().clear_watch(channel_id) +} + +#[command] +pub(crate) async fn check_permissions(app: AppHandle) -> Result { + app.geolocation().check_permissions() +} + +#[command] +pub(crate) async fn request_permissions( + app: AppHandle, + permissions: Option>, +) -> Result { + app.geolocation().request_permissions(permissions) +} diff --git a/plugins/geolocation/src/desktop.rs b/plugins/geolocation/src/desktop.rs new file mode 100644 index 0000000..3ca41db --- /dev/null +++ b/plugins/geolocation/src/desktop.rs @@ -0,0 +1,100 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use serde::{de::DeserializeOwned, Serialize}; +use tauri::{ + ipc::{Channel, InvokeResponseBody}, + plugin::PluginApi, + AppHandle, Runtime, +}; + +use crate::models::*; + +pub fn init( + app: &AppHandle, + _api: PluginApi, +) -> crate::Result> { + Ok(Geolocation(app.clone())) +} + +/// Access to the geolocation APIs. +pub struct Geolocation(AppHandle); + +impl Geolocation { + /// Not implemented on desktop platforms; always resolves to a default, zeroed [`Position`] without reading any real location. + pub fn get_current_position( + &self, + _options: Option, + ) -> crate::Result { + Ok(Position::default()) + } + + /// Not implemented on desktop platforms. Registers a channel for `channel_id` bookkeeping, but `callback` is never invoked with a real [`WatchEvent`]. + pub fn watch_position( + &self, + options: PositionOptions, + callback: F, + ) -> crate::Result { + let channel = Channel::new(move |event| { + let payload = match event { + InvokeResponseBody::Json(payload) => serde_json::from_str::(&payload) + .unwrap_or_else(|error| { + WatchEvent::Error(format!( + "Couldn't deserialize watch event payload: `{error}`" + )) + }), + _ => WatchEvent::Error("Unexpected watch event payload.".to_string()), + }; + + callback(payload); + + Ok(()) + }); + let id = channel.id(); + + self.watch_position_inner(options, channel)?; + + Ok(id) + } + + pub(crate) fn watch_position_inner( + &self, + _options: PositionOptions, + _callback_channel: Channel, + ) -> crate::Result<()> { + Ok(()) + } + + /// Not implemented on desktop platforms; always succeeds without doing anything. + pub fn clear_watch(&self, _channel_id: u32) -> crate::Result<()> { + Ok(()) + } + + /// Not implemented on desktop platforms; always resolves to the default [`PermissionStatus`] (both permissions in the [`Prompt`](tauri::plugin::PermissionState::Prompt) state). + pub fn check_permissions(&self) -> crate::Result { + Ok(PermissionStatus::default()) + } + + /// Not implemented on desktop platforms; always resolves to the default [`PermissionStatus`] without prompting the user. + pub fn request_permissions( + &self, + _permissions: Option>, + ) -> crate::Result { + Ok(PermissionStatus::default()) + } +} + +#[derive(Serialize)] +#[allow(unused)] // TODO: +struct WatchPayload { + options: PositionOptions, + channel: Channel, +} + +#[derive(Serialize)] +#[serde(rename_all = "camelCase")] +#[allow(unused)] // TODO: +struct ClearWatchPayload { + channel_id: u32, +} diff --git a/plugins/geolocation/src/error.rs b/plugins/geolocation/src/error.rs new file mode 100644 index 0000000..1c4f695 --- /dev/null +++ b/plugins/geolocation/src/error.rs @@ -0,0 +1,33 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use serde::{ser::Serializer, Serialize}; + +/// Alias for the result type returned by the geolocation APIs. +pub type Result = std::result::Result; + +// TODO: Improve Error handling (different typed errors instead of one (stringified) PluginInvokeError for all mobile errors) + +/// Errors that can occur while interacting with the geolocation APIs. +#[derive(Debug, thiserror::Error)] +#[cfg_attr(feature = "specta", derive(specta::Type))] +pub enum Error { + /// Forwards an error returned by the underlying Android or iOS plugin invocation. + #[cfg(mobile)] + #[error(transparent)] + PluginInvoke( + #[cfg_attr(feature = "specta", specta(skip))] + #[from] + tauri::plugin::mobile::PluginInvokeError, + ), +} + +impl Serialize for Error { + fn serialize(&self, serializer: S) -> std::result::Result + where + S: Serializer, + { + serializer.serialize_str(self.to_string().as_ref()) + } +} diff --git a/plugins/geolocation/src/lib.rs b/plugins/geolocation/src/lib.rs new file mode 100644 index 0000000..2c87c13 --- /dev/null +++ b/plugins/geolocation/src/lib.rs @@ -0,0 +1,65 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +//! Access device geolocation APIs. +//! +//! ## Cargo features +//! +//! - **specta**: Add support for [`specta::specta`](https://docs.rs/specta/2.0.0-rc.25/specta/attr.specta.html) on structs like [`PermissionStatus`], [`PositionOptions`]. + +use tauri::{ + plugin::{Builder, TauriPlugin}, + Manager, Runtime, +}; + +pub use models::*; + +#[cfg(desktop)] +mod desktop; +#[cfg(mobile)] +mod mobile; + +mod commands; +mod error; +mod models; + +pub use error::{Error, Result}; + +#[cfg(desktop)] +pub use desktop::Geolocation; +#[cfg(mobile)] +pub use mobile::Geolocation; + +/// Extensions to [`tauri::App`], [`tauri::AppHandle`], [`tauri::WebviewWindow`], [`tauri::Webview`] and [`tauri::Window`] to access the geolocation APIs. +pub trait GeolocationExt { + /// Returns the handle to the geolocation APIs. + fn geolocation(&self) -> &Geolocation; +} + +impl> crate::GeolocationExt for T { + fn geolocation(&self) -> &Geolocation { + self.state::>().inner() + } +} + +/// Initializes the plugin. +pub fn init() -> TauriPlugin { + Builder::new("geolocation") + .invoke_handler(tauri::generate_handler![ + commands::get_current_position, + commands::watch_position, + commands::clear_watch, + commands::check_permissions, + commands::request_permissions + ]) + .setup(|app, api| { + #[cfg(mobile)] + let geolocation = mobile::init(app, api)?; + #[cfg(desktop)] + let geolocation = desktop::init(app, api)?; + app.manage(geolocation); + Ok(()) + }) + .build() +} diff --git a/plugins/geolocation/src/mobile.rs b/plugins/geolocation/src/mobile.rs new file mode 100644 index 0000000..4de9676 --- /dev/null +++ b/plugins/geolocation/src/mobile.rs @@ -0,0 +1,123 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use serde::{de::DeserializeOwned, Serialize}; +use tauri::{ + ipc::{Channel, InvokeResponseBody}, + plugin::{PluginApi, PluginHandle}, + AppHandle, Runtime, +}; + +use crate::models::*; + +#[cfg(target_os = "android")] +const PLUGIN_IDENTIFIER: &str = "app.tauri.geolocation"; + +#[cfg(target_os = "ios")] +tauri::ios_plugin_binding!(init_plugin_geolocation); + +// initializes the Kotlin or Swift plugin classes +pub fn init( + _app: &AppHandle, + api: PluginApi, +) -> crate::Result> { + #[cfg(target_os = "android")] + let handle = api.register_android_plugin(PLUGIN_IDENTIFIER, "GeolocationPlugin")?; + #[cfg(target_os = "ios")] + let handle = api.register_ios_plugin(init_plugin_geolocation)?; + Ok(Geolocation(handle)) +} + +/// Access to the geolocation APIs. +pub struct Geolocation(PluginHandle); + +impl Geolocation { + /// Returns the device's current [`Position`]. On Android this returns the last known location immediately if it is still within `maximum_age`, otherwise it requests a fresh reading. Errors if location services are disabled or the required permission was not granted. + pub fn get_current_position( + &self, + options: Option, + ) -> crate::Result { + // TODO: We may have to send over None if that's better on Android + self.0 + .run_mobile_plugin("getCurrentPosition", options.unwrap_or_default()) + .map_err(Into::into) + } + + /// Register a position watcher. This method returns an id to use in `clear_watch`. + pub fn watch_position( + &self, + options: PositionOptions, + callback: F, + ) -> crate::Result { + let channel = Channel::new(move |event| { + let payload = match event { + InvokeResponseBody::Json(payload) => serde_json::from_str::(&payload) + .unwrap_or_else(|error| { + WatchEvent::Error(format!( + "Couldn't deserialize watch event payload: `{error}`" + )) + }), + _ => WatchEvent::Error("Unexpected watch event payload.".to_string()), + }; + + callback(payload); + + Ok(()) + }); + let id = channel.id(); + + self.watch_position_inner(options, channel)?; + + Ok(id) + } + + pub(crate) fn watch_position_inner( + &self, + options: PositionOptions, + channel: Channel, + ) -> crate::Result<()> { + self.0 + .run_mobile_plugin("watchPosition", WatchPayload { options, channel }) + .map_err(Into::into) + } + + /// Removes the position watcher registered with the given `channel_id`, as returned by [`Self::watch_position`]. Stops the platform location updates once no watcher remains. + pub fn clear_watch(&self, channel_id: u32) -> crate::Result<()> { + self.0 + .run_mobile_plugin("clearWatch", ClearWatchPayload { channel_id }) + .map_err(Into::into) + } + + /// Returns the current [`PermissionStatus`] for the geolocation APIs. Errors if location services are disabled on the device. + pub fn check_permissions(&self) -> crate::Result { + self.0 + .run_mobile_plugin("checkPermissions", ()) + .map_err(Into::into) + } + + /// Requests the given `permissions` (or all of them when `None`) and returns the resulting [`PermissionStatus`]. Errors if location services are disabled on the device. + pub fn request_permissions( + &self, + permissions: Option>, + ) -> crate::Result { + self.0 + .run_mobile_plugin( + "requestPermissions", + serde_json::json!({ "permissions": permissions }), + ) + .map_err(Into::into) + } +} + +#[derive(Serialize)] +struct WatchPayload { + options: PositionOptions, + channel: Channel, +} + +#[derive(Serialize)] +#[serde(rename_all = "camelCase")] +struct ClearWatchPayload { + channel_id: u32, +} diff --git a/plugins/geolocation/src/models.rs b/plugins/geolocation/src/models.rs new file mode 100644 index 0000000..9dd7d3a --- /dev/null +++ b/plugins/geolocation/src/models.rs @@ -0,0 +1,106 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use serde::{Deserialize, Serialize}; +use tauri::plugin::PermissionState; + +/// The current permission state for the geolocation APIs. +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[cfg_attr(feature = "specta", derive(specta::Type))] +#[serde(rename_all = "camelCase")] +pub struct PermissionStatus { + /// Permission state for the location alias. + /// + /// On Android it requests/checks both ACCESS_COARSE_LOCATION and ACCESS_FINE_LOCATION permissions. + /// + /// On iOS it requests/checks location permissions. + pub location: PermissionState, + /// Permissions state for the coarseLoaction alias. + /// + /// On Android it requests/checks ACCESS_COARSE_LOCATION. + /// + /// On Android 12+, users can choose between Approximate location (ACCESS_COARSE_LOCATION) and Precise location (ACCESS_FINE_LOCATION). + /// + /// On iOS it will have the same value as the `location` alias. + pub coarse_location: PermissionState, +} + +/// Options used to configure a [`get_current_position`](crate::Geolocation::get_current_position) or [`watch_position`](crate::Geolocation::watch_position) request. +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[cfg_attr(feature = "specta", derive(specta::Type))] +#[serde(rename_all = "camelCase")] +pub struct PositionOptions { + /// High accuracy mode (such as GPS, if available) + /// Will be ignored on Android 12+ if users didn't grant the ACCESS_FINE_LOCATION permission. + pub enable_high_accuracy: bool, + /// The maximum wait time in milliseconds for location updates. + /// Default: 10000 + /// On Android the timeout gets ignored for getCurrentPosition. + /// Ignored on iOS. + // TODO: Handle Infinity and default to it. + // TODO: Should be u64+ but specta doesn't like that? + pub timeout: u32, + /// The maximum age in milliseconds of a possible cached position that is acceptable to return. + /// Default: 0 + /// Ignored on iOS. + // TODO: Handle Infinity. + // TODO: Should be u64+ but specta doesn't like that? + pub maximum_age: u32, +} + +/// The individual permission aliases that can be requested with [`request_permissions`](crate::Geolocation::request_permissions). +#[derive(Debug, Clone, Serialize, Deserialize)] +#[cfg_attr(feature = "specta", derive(specta::Type))] +#[serde(rename_all = "camelCase")] +pub enum PermissionType { + /// The `location` alias. On Android this maps to both `ACCESS_COARSE_LOCATION` and `ACCESS_FINE_LOCATION`. On iOS it maps to the standard location permission. + Location, + /// The `coarseLocation` alias. On Android this maps to `ACCESS_COARSE_LOCATION` only. On iOS it behaves the same as [`Location`](Self::Location). + CoarseLocation, +} + +/// The GPS coordinates of a [`Position`], along with the accuracy of each reading. +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[cfg_attr(feature = "specta", derive(specta::Type))] +#[serde(rename_all = "camelCase")] +pub struct Coordinates { + /// Latitude in decimal degrees. + pub latitude: f64, + /// Longitude in decimal degrees. + pub longitude: f64, + /// Accuracy level of the latitude and longitude coordinates in meters. + pub accuracy: f64, + /// Accuracy level of the altitude coordinate in meters, if available. + /// Available on all iOS versions and on Android 8 and above. + pub altitude_accuracy: Option, + /// The altitude the user is at, if available. + pub altitude: Option, + /// The speed the user is traveling, in meters per second, if available. + pub speed: Option, + /// The heading the user is facing, if available. + pub heading: Option, +} + +/// A geolocation reading, as returned by [`get_current_position`](crate::Geolocation::get_current_position) and reported through [`WatchEvent::Position`]. +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[cfg_attr(feature = "specta", derive(specta::Type))] +#[serde(rename_all = "camelCase")] +pub struct Position { + /// Creation time for these coordinates. + // TODO: Check if we're actually losing precision. + pub timestamp: u64, + /// The GPS coordinates along with the accuracy of the data. + pub coords: Coordinates, +} + +/// A single update sent through the channel callback registered with [`watch_position`](crate::Geolocation::watch_position). +#[derive(Debug, Clone, Serialize, Deserialize)] +#[cfg_attr(feature = "specta", derive(specta::Type))] +#[serde(untagged)] +pub enum WatchEvent { + /// A new position was read successfully. + Position(Position), + /// The platform failed to read a position; the string is the platform-provided error message. + Error(String), +} diff --git a/plugins/geolocation/tsconfig.json b/plugins/geolocation/tsconfig.json new file mode 100644 index 0000000..4374468 --- /dev/null +++ b/plugins/geolocation/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "rootDir": "guest-js" + }, + "include": ["guest-js/*.ts"] +} diff --git a/plugins/global-shortcut/CHANGELOG.md b/plugins/global-shortcut/CHANGELOG.md new file mode 100644 index 0000000..4ac2141 --- /dev/null +++ b/plugins/global-shortcut/CHANGELOG.md @@ -0,0 +1,126 @@ +# Changelog + +## \[2.3.2] + +- [`cb863d57`](https://github.com/tauri-apps/plugins-workspace/commit/cb863d57c60790efc41b2bd77d065bd91b4ef2b1) ([#3408](https://github.com/tauri-apps/plugins-workspace/pull/3408) by [@renovate](https://github.com/tauri-apps/plugins-workspace/../../renovate)) Update crate `global-hotkey` to 0.8 to fix cpu usage while pressing the keys on windows and to add F13-F24 mappings on Linux/X11. + +## \[2.3.1] + +- [`93426f85`](https://github.com/tauri-apps/plugins-workspace/commit/93426f85120f49beb9f40222bff45185a32d54a9) Fixed an issue that caused docs.rs builds to fail. No user facing changes. + +## \[2.3.0] + +- [`f209b2f2`](https://github.com/tauri-apps/plugins-workspace/commit/f209b2f23cb29133c97ad5961fb46ef794dbe063) ([#2804](https://github.com/tauri-apps/plugins-workspace/pull/2804) by [@renovate](https://github.com/tauri-apps/plugins-workspace/../../renovate)) Updated tauri to 2.6 + +## \[2.2.1] + +- [`494d1fea`](https://github.com/tauri-apps/plugins-workspace/commit/494d1fea137ffd60da98b25305c9d666df62cc63) ([#2684](https://github.com/tauri-apps/plugins-workspace/pull/2684) by [@renovate](https://github.com/tauri-apps/plugins-workspace/../../renovate)) Updated `global-hotkey` crate to `0.7` to fix a panic when trying to register a key combination which was already registered by another program. No API changes. + +## \[2.2.0] + +- [`3a79266b`](https://github.com/tauri-apps/plugins-workspace/commit/3a79266b8cf96a55b1ae6339d725567d45a44b1d) ([#2173](https://github.com/tauri-apps/plugins-workspace/pull/2173) by [@FabianLars](https://github.com/tauri-apps/plugins-workspace/../../FabianLars)) Bumped all plugins to `v2.2.0`. From now, the versions for the Rust and JavaScript packages of each plugin will be in sync with each other. + +## \[2.0.1] + +- [`a1a82208`](https://github.com/tauri-apps/plugins-workspace/commit/a1a82208ed4ab87f83310be0dc95428aec9ab241) ([#1873](https://github.com/tauri-apps/plugins-workspace/pull/1873) by [@lucasfernog](https://github.com/tauri-apps/plugins-workspace/../../lucasfernog)) Downgrade MSRV to 1.77.2 to support Windows 7. + +## \[2.0.0] + +- [`e2c4dfb6`](https://github.com/tauri-apps/plugins-workspace/commit/e2c4dfb6af43e5dd8d9ceba232c315f5febd55c1) Update to tauri v2 stable release. + +## \[2.0.0-rc.1] + +- [`e2e97db5`](https://github.com/tauri-apps/plugins-workspace/commit/e2e97db51983267f5be84d4f6f0278d58834d1f5) ([#1701](https://github.com/tauri-apps/plugins-workspace/pull/1701) by [@lucasfernog](https://github.com/tauri-apps/plugins-workspace/../../lucasfernog)) Update to tauri 2.0.0-rc.8 + +## \[2.0.0-rc.1] + +- [`2c00c029`](https://github.com/tauri-apps/plugins-workspace/commit/2c00c0292c9127b81567de46691e8c0f73557261) ([#1630](https://github.com/tauri-apps/plugins-workspace/pull/1630) by [@FabianLars](https://github.com/tauri-apps/plugins-workspace/../../FabianLars)) Fixed an issue that caused multi-word IIFE names to not be formatted correctly. For example the `barcode-scanner` was defined as `window.__TAURI_PLUGIN_CLIPBOARDMANAGER__` instead of `window.__TAURI_PLUGIN_CLIPBOARD_MANAGER__`. + +## \[2.0.0-rc.0] + +- [`9887d1`](https://github.com/tauri-apps/plugins-workspace/commit/9887d14bd0e971c4c0f5c1188fc4005d3fc2e29e) Update to tauri RC. + +## \[2.0.0-beta.8] + +- [`99d6ac0f`](https://github.com/tauri-apps/plugins-workspace/commit/99d6ac0f9506a6a4a1aa59c728157190a7441af6) ([#1606](https://github.com/tauri-apps/plugins-workspace/pull/1606) by [@FabianLars](https://github.com/tauri-apps/plugins-workspace/../../FabianLars)) The JS packages now specify the *minimum* `@tauri-apps/api` version instead of a single exact version. +- [`6de87966`](https://github.com/tauri-apps/plugins-workspace/commit/6de87966ecc00ad9d91c25be452f1f46bd2b7e1f) ([#1597](https://github.com/tauri-apps/plugins-workspace/pull/1597) by [@Legend-Master](https://github.com/tauri-apps/plugins-workspace/../../Legend-Master)) Update to tauri beta.25. + +## \[2.0.0-beta.7] + +- [`381a466d`](https://github.com/tauri-apps/plugins-workspace/commit/381a466db344e59a76b2a4d5785b2a0b64d4d373) ([#1117](https://github.com/tauri-apps/plugins-workspace/pull/1117) by [@amrbashir](https://github.com/tauri-apps/plugins-workspace/../../amrbashir)) Refactored the JS APIs: + + - Enhanced `register` and `unregister` to take either a single shortcut or an array. + - Removed `registerAll` instead use `register` with an array. +- [`22a17980`](https://github.com/tauri-apps/plugins-workspace/commit/22a17980ff4f6f8c40adb1b8f4ffc6dae2fe7e30) ([#1537](https://github.com/tauri-apps/plugins-workspace/pull/1537) by [@lucasfernog](https://github.com/tauri-apps/plugins-workspace/../../lucasfernog)) Update to tauri beta.24. + +## \[2.0.0-beta.6] + +- [`76daee7a`](https://github.com/tauri-apps/plugins-workspace/commit/76daee7aafece34de3092c86e531cf9eb1138989) ([#1512](https://github.com/tauri-apps/plugins-workspace/pull/1512) by [@renovate](https://github.com/tauri-apps/plugins-workspace/../../renovate)) Update to tauri beta.23. + +## \[2.0.0-beta.5] + +- [`9013854f`](https://github.com/tauri-apps/plugins-workspace/commit/9013854f42a49a230b9dbb9d02774765528a923f)([#1382](https://github.com/tauri-apps/plugins-workspace/pull/1382)) Update to tauri beta.22. + +## \[2.0.0-beta.4] + +- [`430bd6f4`](https://github.com/tauri-apps/plugins-workspace/commit/430bd6f4f379bee5d232ae6b098ae131db7f178a)([#1363](https://github.com/tauri-apps/plugins-workspace/pull/1363)) Update to tauri beta.20. + +## \[2.0.0-beta.3] + +- [`9c7eb359`](https://github.com/tauri-apps/plugins-workspace/commit/9c7eb35967ad10659eb4976bd6f77d9d270bfeed)([#1244](https://github.com/tauri-apps/plugins-workspace/pull/1244)) Refactored APIs to introduce new pressed and released events: + + - Added `ShortcutEvent` and `ShortcutState` types in Rust. + - Changed the handler function passed to `GlobalShortcut::on_shortcut`, `GlobalShortcut::on_all_shortcuts` and `Builder::with_handler` to take a 3rd argument of type `ShortcutEvent`. + - Added `ShortcutEvent` interface in JS. + - Changed `ShortcutHandler` type alias (which affects the JS `register` and `registerAll` APIs) to take `ShortcutEvent` instead of a string. +- [`bd1ed590`](https://github.com/tauri-apps/plugins-workspace/commit/bd1ed5903ffcce5500310dac1e59e8c67674ef1e)([#1237](https://github.com/tauri-apps/plugins-workspace/pull/1237)) Update to tauri beta.17. + +## \[2.0.0-beta.3] + +- [`a04ea2f`](https://github.com/tauri-apps/plugins-workspace/commit/a04ea2f38294d5a3987578283badc8eec87a7752)([#1071](https://github.com/tauri-apps/plugins-workspace/pull/1071)) The global API script is now only added to the binary when the `withGlobalTauri` config is true. +- [`62dafda`](https://github.com/tauri-apps/plugins-workspace/commit/62dafda6526899b407a7c5a1bb269c5c0dfb2630)([#969](https://github.com/tauri-apps/plugins-workspace/pull/969)) **Breaking change** Refactored the plugin Rust APIs for better DX and flexibility: + + - Changed `Builder::with_handler` to be a method instead of a static method, it will also be triggered for any and all shortcuts even if the shortcut is registered through JS. + - Added `Builder::with_shortcut` and `Builder::with_shortcuts` to register shortcuts on the plugin builder. + - Added `on_shortcut` and `on_all_shortcuts` to register shortcuts with a handler. + +## \[2.0.0-beta.2] + +- [`99bea25`](https://github.com/tauri-apps/plugins-workspace/commit/99bea2559c2c0648c2519c50a18cd124dacef57b)([#1005](https://github.com/tauri-apps/plugins-workspace/pull/1005)) Update to tauri beta.8. + +## \[2.0.0-beta.1] + +- [`569defb`](https://github.com/tauri-apps/plugins-workspace/commit/569defbe9492e38938554bb7bdc1be9151456d21) Update to tauri beta.4. + +## \[2.0.0-beta.0] + +- [`d198c01`](https://github.com/tauri-apps/plugins-workspace/commit/d198c014863ee260cb0de88a14b7fc4356ef7474)([#862](https://github.com/tauri-apps/plugins-workspace/pull/862)) Update to tauri beta. +- [`d198c01`](https://github.com/tauri-apps/plugins-workspace/commit/d198c014863ee260cb0de88a14b7fc4356ef7474)([#862](https://github.com/tauri-apps/plugins-workspace/pull/862)) Add permissions. + +## \[2.0.0-alpha.5] + +- [`387c2f9`](https://github.com/tauri-apps/plugins-workspace/commit/387c2f9e0ce4c75c07ffa3fd76391a25b58f5daf)([#802](https://github.com/tauri-apps/plugins-workspace/pull/802)) Update to @tauri-apps/api v2.0.0-alpha.13. + +## \[2.0.0-alpha.4] + +- [`387c2f9`](https://github.com/tauri-apps/plugins-workspace/commit/387c2f9e0ce4c75c07ffa3fd76391a25b58f5daf)([#802](https://github.com/tauri-apps/plugins-workspace/pull/802)) Update to @tauri-apps/api v2.0.0-alpha.12. + +## \[2.0.0-alpha.3] + +- [`e438e0a`](https://github.com/tauri-apps/plugins-workspace/commit/e438e0a62d4b430a5159f05f13ecd397dd891a0d)([#676](https://github.com/tauri-apps/plugins-workspace/pull/676)) Update to @tauri-apps/api v2.0.0-alpha.11. + +## \[2.0.0-alpha.2] + +- [`5c13736`](https://github.com/tauri-apps/plugins-workspace/commit/5c137365c60790e8d4037d449e8237aa3fffdab0)([#673](https://github.com/tauri-apps/plugins-workspace/pull/673)) Update to @tauri-apps/api v2.0.0-alpha.9. + +## \[2.0.0-alpha.2] + +- [`4e2cef9`](https://github.com/tauri-apps/plugins-workspace/commit/4e2cef9b702bbbb9cf4ee17de50791cb21f1b2a4)([#593](https://github.com/tauri-apps/plugins-workspace/pull/593)) Update to alpha.12. + +## \[2.0.0-alpha.1] + +- [`d74fc0a`](https://github.com/tauri-apps/plugins-workspace/commit/d74fc0a097996e90a37be8f57d50b7d1f6ca616f)([#555](https://github.com/tauri-apps/plugins-workspace/pull/555)) Update to alpha.11. + +## \[2.0.0-alpha.0] + +- [`717ae67`](https://github.com/tauri-apps/plugins-workspace/commit/717ae670978feb4492fac1f295998b93f2b9347f)([#371](https://github.com/tauri-apps/plugins-workspace/pull/371)) First v2 alpha release! diff --git a/plugins/global-shortcut/Cargo.toml b/plugins/global-shortcut/Cargo.toml new file mode 100644 index 0000000..6d31703 --- /dev/null +++ b/plugins/global-shortcut/Cargo.toml @@ -0,0 +1,37 @@ +[package] +name = "tauri-plugin-global-shortcut" +version = "2.3.2" +description = "Register global hotkeys listeners on your Tauri application." +edition = { workspace = true } +authors = { workspace = true } +license = { workspace = true } +rust-version = { workspace = true } +repository = { workspace = true } +links = "tauri-plugin-global-shortcut" +exclude = [ + "/banner.png", + "/guest-js", + "/package.json", + "/rollup.config.js", + "/tsconfig.json", +] + +[package.metadata.platforms.support] +windows = { level = "full", notes = "" } +linux = { level = "full", notes = "" } +macos = { level = "full", notes = "" } +android = { level = "none", notes = "" } +ios = { level = "none", notes = "" } + +[build-dependencies] +tauri-plugin = { workspace = true, features = ["build"] } + +[dependencies] +serde = { workspace = true } +serde_json = { workspace = true } +tauri = { workspace = true } +log = { workspace = true } +thiserror = { workspace = true } + +[target."cfg(not(any(target_os = \"android\", target_os = \"ios\")))".dependencies] +global-hotkey = { version = "0.8", features = ["serde"] } diff --git a/plugins/global-shortcut/LICENSE.spdx b/plugins/global-shortcut/LICENSE.spdx new file mode 100644 index 0000000..cdd0df5 --- /dev/null +++ b/plugins/global-shortcut/LICENSE.spdx @@ -0,0 +1,20 @@ +SPDXVersion: SPDX-2.1 +DataLicense: CC0-1.0 +PackageName: tauri +DataFormat: SPDXRef-1 +PackageSupplier: Organization: The Tauri Programme in the Commons Conservancy +PackageHomePage: https://tauri.app +PackageLicenseDeclared: Apache-2.0 +PackageLicenseDeclared: MIT +PackageCopyrightText: 2019-2022, The Tauri Programme in the Commons Conservancy +PackageSummary: Tauri is a rust project that enables developers to make secure +and small desktop applications using a web frontend. + +PackageComment: The package includes the following libraries; see +Relationship information. + +Created: 2019-05-20T09:00:00Z +PackageDownloadLocation: git://github.com/tauri-apps/tauri +PackageDownloadLocation: git+https://github.com/tauri-apps/tauri.git +PackageDownloadLocation: git+ssh://github.com/tauri-apps/tauri.git +Creator: Person: Daniel Thompson-Yvetot \ No newline at end of file diff --git a/plugins/global-shortcut/LICENSE_APACHE-2.0 b/plugins/global-shortcut/LICENSE_APACHE-2.0 new file mode 100644 index 0000000..4947287 --- /dev/null +++ b/plugins/global-shortcut/LICENSE_APACHE-2.0 @@ -0,0 +1,177 @@ + + 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 \ No newline at end of file diff --git a/plugins/global-shortcut/LICENSE_MIT b/plugins/global-shortcut/LICENSE_MIT new file mode 100644 index 0000000..4d75472 --- /dev/null +++ b/plugins/global-shortcut/LICENSE_MIT @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2017 - Present Tauri Apps 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. \ No newline at end of file diff --git a/plugins/global-shortcut/README.md b/plugins/global-shortcut/README.md new file mode 100644 index 0000000..a988826 --- /dev/null +++ b/plugins/global-shortcut/README.md @@ -0,0 +1,119 @@ +![plugin-global-shortcut](https://github.com/tauri-apps/plugins-workspace/raw/v2/plugins/global-shortcut/banner.png) + +Register global shortcuts. + +| Platform | Supported | +| -------- | --------- | +| Linux | ✓ | +| Windows | ✓ | +| macOS | ✓ | +| Android | x | +| iOS | x | + +## Install + +_This plugin requires a Rust version of at least **1.77.2**_ + +There are three general methods of installation that we can recommend. + +1. Use crates.io and npm (easiest, and requires you to trust that our publishing pipeline worked) +2. Pull sources directly from Github using git tags / revision hashes (most secure) +3. Git submodule install this repo in your tauri project and then use file protocol to ingest the source (most secure, but inconvenient to use) + +Install the Core plugin by adding the following to your `Cargo.toml` file: + +`src-tauri/Cargo.toml` + +```toml +# you can add the dependencies on the `[dependencies]` section if you do not target mobile +[target."cfg(not(any(target_os = \"android\", target_os = \"ios\")))".dependencies] +tauri-plugin-global-shortcut = "2.0.0" +# alternatively with Git: +tauri-plugin-global-shortcut = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v2" } +``` + +You can install the JavaScript Guest bindings using your preferred JavaScript package manager: + +```sh +pnpm add @tauri-apps/plugin-global-shortcut +# or +npm add @tauri-apps/plugin-global-shortcut +# or +yarn add @tauri-apps/plugin-global-shortcut +``` + +## Usage + +First you need to register the core plugin with Tauri: + +`src-tauri/src/lib.rs` + +```rust +fn main() { + tauri::Builder::default() + .setup(|app| { + #[cfg(desktop)] + { + use tauri::Emitter; + use tauri_plugin_global_shortcut::{Code, Modifiers, ShortcutState}; + + app.handle().plugin( + tauri_plugin_global_shortcut::Builder::new() + .with_shortcuts(["ctrl+d", "alt+space"])? + .with_handler(|app, shortcut, event| { + if event.state == ShortcutState::Pressed { + if shortcut.matches(Modifiers::CONTROL, Code::KeyD) { + let _ = app.emit("shortcut-event", "Ctrl+D triggered"); + } + if shortcut.matches(Modifiers::ALT, Code::Space) { + let _ = app.emit("shortcut-event", "Alt+Space triggered"); + } + } + }) + .build(), + )?; + } + + Ok(()) + }) + .run(tauri::generate_context!()) + .expect("error while running tauri application"); +} +``` + +Afterwards all the plugin's APIs are available through the JavaScript bindings: + +```javascript +import { register } from '@tauri-apps/plugin-global-shortcut' +await register('CommandOrControl+Shift+C', (event) => { + if (event.state === 'Pressed') { + console.log('Shortcut triggered') + } +}) +``` + +## Contributing + +PRs accepted. Please make sure to read the Contributing Guide before making a pull request. + +## Partners + + + + + + + +
+ + CrabNebula + +
+ +For the complete list of sponsors please visit our [website](https://tauri.app#sponsors) and [Open Collective](https://opencollective.com/tauri). + +## License + +Code: (c) 2015 - Present - The Tauri Programme within The Commons Conservancy. + +MIT or MIT/Apache 2.0 where applicable. diff --git a/plugins/global-shortcut/SECURITY.md b/plugins/global-shortcut/SECURITY.md new file mode 100644 index 0000000..4f09bba --- /dev/null +++ b/plugins/global-shortcut/SECURITY.md @@ -0,0 +1,23 @@ +# Security Policy + +**Do not report security vulnerabilities through public GitHub issues.** + +**Please use the [Private Vulnerability Disclosure](https://docs.github.com/en/code-security/security-advisories/guidance-on-reporting-and-writing-information-about-vulnerabilities/privately-reporting-a-security-vulnerability#privately-reporting-a-security-vulnerability) feature of GitHub.** + +Include as much of the following information: + +- Type of issue (e.g. improper input parsing, privilege escalation, etc.) +- The location of the affected source code (tag/branch/commit or direct URL) +- Any special configuration required to reproduce the issue +- The distribution affected or used to help us with reproduction of the issue +- Step-by-step instructions to reproduce the issue +- Ideally a reproduction repository +- Impact of the issue, including how an attacker might exploit the issue + +We prefer to receive reports in English. + +## Contact + +Please disclose a vulnerability or security relevant issue here: [https://github.com/tauri-apps/plugins-workspace/security/advisories/new](https://github.com/tauri-apps/plugins-workspace/security/advisories/new). + +Alternatively, you can also contact us by email via [security@tauri.app](mailto:security@tauri.app). diff --git a/plugins/global-shortcut/api-iife.js b/plugins/global-shortcut/api-iife.js new file mode 100644 index 0000000..6e93962 --- /dev/null +++ b/plugins/global-shortcut/api-iife.js @@ -0,0 +1 @@ +if("__TAURI__"in window){var __TAURI_PLUGIN_GLOBAL_SHORTCUT__=function(r,t){"use strict";return r.isRegistered=async function(r){return await t.invoke("plugin:global-shortcut|is_registered",{shortcut:r})},r.register=async function(r,n){const e=new t.Channel;return e.onmessage=n,await t.invoke("plugin:global-shortcut|register",{shortcuts:Array.isArray(r)?r:[r],handler:e})},r.unregister=async function(r){return await t.invoke("plugin:global-shortcut|unregister",{shortcuts:Array.isArray(r)?r:[r]})},r.unregisterAll=async function(){return await t.invoke("plugin:global-shortcut|unregister_all",{})},r}({},window.__TAURI__.core);Object.defineProperty(window.__TAURI__,"globalShortcut",{value:__TAURI_PLUGIN_GLOBAL_SHORTCUT__})} diff --git a/plugins/global-shortcut/banner.png b/plugins/global-shortcut/banner.png new file mode 100644 index 0000000000000000000000000000000000000000..d74203a56b34875dce8af509bcd19b41593dfef2 GIT binary patch literal 64995 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 void + +/** + * Register a global shortcut or a list of shortcuts. + * + * The handler is called when any of the registered shortcuts are pressed by the user. + * + * If the shortcut is already taken by another application, the handler will not be triggered. + * Make sure the shortcut is as unique as possible while still taking user experience into consideration. + * + * @example + * ```typescript + * import { register } from '@tauri-apps/plugin-global-shortcut'; + * + * // register a single hotkey + * await register('CommandOrControl+Shift+C', (event) => { + * if (event.state === "Pressed") { + * console.log('Shortcut triggered'); + * } + * }); + * + * // or register multiple hotkeys at once + * await register(['CommandOrControl+Shift+C', 'Alt+A'], (event) => { + * console.log(`Shortcut ${event.shortcut} triggered`); + * }); + * ``` + * + * @param shortcuts A shortcut definition, or a list of shortcut definitions, with modifiers and key separated by "+" e.g. CmdOrControl+Q + * @param handler Shortcut handler callback - takes the triggered shortcut as argument + * + * @since 2.0.0 + */ +async function register( + shortcuts: string | string[], + handler: ShortcutHandler +): Promise { + const h = new Channel() + h.onmessage = handler + + return await invoke('plugin:global-shortcut|register', { + shortcuts: Array.isArray(shortcuts) ? shortcuts : [shortcuts], + handler: h + }) +} + +/** + * Unregister a global shortcut or a list of shortcuts. + * + * @example + * ```typescript + * import { unregister } from '@tauri-apps/plugin-global-shortcut'; + * + * // unregister a single hotkey + * await unregister('CmdOrControl+Space'); + * + * // or unregister multiple hotkeys at the same time + * await unregister(['CmdOrControl+Space', 'Alt+A']); + * ``` + * + * @param shortcuts A shortcut definition, or a list of shortcut definitions, with modifiers and key separated by "+" e.g. CmdOrControl+Q + * + * @since 2.0.0 + */ +async function unregister(shortcuts: string | string[]): Promise { + return await invoke('plugin:global-shortcut|unregister', { + shortcuts: Array.isArray(shortcuts) ? shortcuts : [shortcuts] + }) +} + +/** + * Unregister all global shortcuts. + * + * @example + * ```typescript + * import { unregisterAll } from '@tauri-apps/plugin-global-shortcut'; + * await unregisterAll(); + * ``` + * @since 2.0.0 + */ +async function unregisterAll(): Promise { + return await invoke('plugin:global-shortcut|unregister_all', {}) +} + +/** + * Determines whether the given shortcut is registered by this application or not. + * + * If the shortcut is registered by another application, it will still return `false`. + * + * @example + * ```typescript + * import { isRegistered } from '@tauri-apps/plugin-global-shortcut'; + * const isRegistered = await isRegistered('CommandOrControl+P'); + * ``` + * + * @param shortcut shortcut definition, modifiers and key separated by "+" e.g. CmdOrControl+Q + * @returns A promise resolving to whether the shortcut is currently registered by this application. + * + * @since 2.0.0 + */ +async function isRegistered(shortcut: string): Promise { + return await invoke('plugin:global-shortcut|is_registered', { + shortcut + }) +} + +export { register, unregister, unregisterAll, isRegistered } diff --git a/plugins/global-shortcut/package.json b/plugins/global-shortcut/package.json new file mode 100644 index 0000000..21d2920 --- /dev/null +++ b/plugins/global-shortcut/package.json @@ -0,0 +1,29 @@ +{ + "name": "@tauri-apps/plugin-global-shortcut", + "version": "2.3.2", + "license": "MIT OR Apache-2.0", + "authors": [ + "Tauri Programme within The Commons Conservancy" + ], + "repository": "https://github.com/tauri-apps/plugins-workspace", + "type": "module", + "types": "./dist-js/index.d.ts", + "main": "./dist-js/index.cjs", + "module": "./dist-js/index.js", + "exports": { + "types": "./dist-js/index.d.ts", + "import": "./dist-js/index.js", + "require": "./dist-js/index.cjs" + }, + "scripts": { + "build": "rollup -c" + }, + "files": [ + "dist-js", + "README.md", + "LICENSE" + ], + "dependencies": { + "@tauri-apps/api": "^2.11.0" + } +} diff --git a/plugins/global-shortcut/permissions/autogenerated/commands/is_registered.toml b/plugins/global-shortcut/permissions/autogenerated/commands/is_registered.toml new file mode 100644 index 0000000..2dd73ac --- /dev/null +++ b/plugins/global-shortcut/permissions/autogenerated/commands/is_registered.toml @@ -0,0 +1,13 @@ +# Automatically generated - DO NOT EDIT! + +"$schema" = "../../schemas/schema.json" + +[[permission]] +identifier = "allow-is-registered" +description = "Enables the is_registered command without any pre-configured scope." +commands.allow = ["is_registered"] + +[[permission]] +identifier = "deny-is-registered" +description = "Denies the is_registered command without any pre-configured scope." +commands.deny = ["is_registered"] diff --git a/plugins/global-shortcut/permissions/autogenerated/commands/register.toml b/plugins/global-shortcut/permissions/autogenerated/commands/register.toml new file mode 100644 index 0000000..4eec17d --- /dev/null +++ b/plugins/global-shortcut/permissions/autogenerated/commands/register.toml @@ -0,0 +1,13 @@ +# Automatically generated - DO NOT EDIT! + +"$schema" = "../../schemas/schema.json" + +[[permission]] +identifier = "allow-register" +description = "Enables the register command without any pre-configured scope." +commands.allow = ["register"] + +[[permission]] +identifier = "deny-register" +description = "Denies the register command without any pre-configured scope." +commands.deny = ["register"] diff --git a/plugins/global-shortcut/permissions/autogenerated/commands/register_all.toml b/plugins/global-shortcut/permissions/autogenerated/commands/register_all.toml new file mode 100644 index 0000000..df5d0ec --- /dev/null +++ b/plugins/global-shortcut/permissions/autogenerated/commands/register_all.toml @@ -0,0 +1,13 @@ +# Automatically generated - DO NOT EDIT! + +"$schema" = "../../schemas/schema.json" + +[[permission]] +identifier = "allow-register-all" +description = "Enables the register_all command without any pre-configured scope." +commands.allow = ["register_all"] + +[[permission]] +identifier = "deny-register-all" +description = "Denies the register_all command without any pre-configured scope." +commands.deny = ["register_all"] diff --git a/plugins/global-shortcut/permissions/autogenerated/commands/unregister.toml b/plugins/global-shortcut/permissions/autogenerated/commands/unregister.toml new file mode 100644 index 0000000..5d33c97 --- /dev/null +++ b/plugins/global-shortcut/permissions/autogenerated/commands/unregister.toml @@ -0,0 +1,13 @@ +# Automatically generated - DO NOT EDIT! + +"$schema" = "../../schemas/schema.json" + +[[permission]] +identifier = "allow-unregister" +description = "Enables the unregister command without any pre-configured scope." +commands.allow = ["unregister"] + +[[permission]] +identifier = "deny-unregister" +description = "Denies the unregister command without any pre-configured scope." +commands.deny = ["unregister"] diff --git a/plugins/global-shortcut/permissions/autogenerated/commands/unregister_all.toml b/plugins/global-shortcut/permissions/autogenerated/commands/unregister_all.toml new file mode 100644 index 0000000..12c12f8 --- /dev/null +++ b/plugins/global-shortcut/permissions/autogenerated/commands/unregister_all.toml @@ -0,0 +1,13 @@ +# Automatically generated - DO NOT EDIT! + +"$schema" = "../../schemas/schema.json" + +[[permission]] +identifier = "allow-unregister-all" +description = "Enables the unregister_all command without any pre-configured scope." +commands.allow = ["unregister_all"] + +[[permission]] +identifier = "deny-unregister-all" +description = "Denies the unregister_all command without any pre-configured scope." +commands.deny = ["unregister_all"] diff --git a/plugins/global-shortcut/permissions/autogenerated/reference.md b/plugins/global-shortcut/permissions/autogenerated/reference.md new file mode 100644 index 0000000..240224c --- /dev/null +++ b/plugins/global-shortcut/permissions/autogenerated/reference.md @@ -0,0 +1,146 @@ +## Default Permission + +No features are enabled by default, as we believe +the shortcuts can be inherently dangerous and it is +application specific if specific shortcuts should be +registered or unregistered. + +## Permission Table + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
IdentifierDescription
+ +`global-shortcut:allow-is-registered` + + + +Enables the is_registered command without any pre-configured scope. + +
+ +`global-shortcut:deny-is-registered` + + + +Denies the is_registered command without any pre-configured scope. + +
+ +`global-shortcut:allow-register` + + + +Enables the register command without any pre-configured scope. + +
+ +`global-shortcut:deny-register` + + + +Denies the register command without any pre-configured scope. + +
+ +`global-shortcut:allow-register-all` + + + +Enables the register_all command without any pre-configured scope. + +
+ +`global-shortcut:deny-register-all` + + + +Denies the register_all command without any pre-configured scope. + +
+ +`global-shortcut:allow-unregister` + + + +Enables the unregister command without any pre-configured scope. + +
+ +`global-shortcut:deny-unregister` + + + +Denies the unregister command without any pre-configured scope. + +
+ +`global-shortcut:allow-unregister-all` + + + +Enables the unregister_all command without any pre-configured scope. + +
+ +`global-shortcut:deny-unregister-all` + + + +Denies the unregister_all command without any pre-configured scope. + +
diff --git a/plugins/global-shortcut/permissions/default.toml b/plugins/global-shortcut/permissions/default.toml new file mode 100644 index 0000000..fd32c51 --- /dev/null +++ b/plugins/global-shortcut/permissions/default.toml @@ -0,0 +1,10 @@ +"$schema" = "schemas/schema.json" +[default] +description = """ +No features are enabled by default, as we believe +the shortcuts can be inherently dangerous and it is +application specific if specific shortcuts should be +registered or unregistered. +""" + +permissions = [] diff --git a/plugins/global-shortcut/permissions/schemas/schema.json b/plugins/global-shortcut/permissions/schemas/schema.json new file mode 100644 index 0000000..1224869 --- /dev/null +++ b/plugins/global-shortcut/permissions/schemas/schema.json @@ -0,0 +1,366 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "PermissionFile", + "description": "Permission file that can define a default permission, a set of permissions or a list of inlined permissions.", + "type": "object", + "properties": { + "default": { + "description": "The default permission set for the plugin", + "anyOf": [ + { + "$ref": "#/definitions/DefaultPermission" + }, + { + "type": "null" + } + ] + }, + "set": { + "description": "A list of permissions sets defined", + "type": "array", + "items": { + "$ref": "#/definitions/PermissionSet" + } + }, + "permission": { + "description": "A list of inlined permissions", + "default": [], + "type": "array", + "items": { + "$ref": "#/definitions/Permission" + } + } + }, + "definitions": { + "DefaultPermission": { + "description": "The default permission set of the plugin.\n\nWorks similarly to a permission with the \"default\" identifier.", + "type": "object", + "required": [ + "permissions" + ], + "properties": { + "version": { + "description": "The version of the permission.", + "type": [ + "integer", + "null" + ], + "format": "uint64", + "minimum": 1.0 + }, + "description": { + "description": "Human-readable description of what the permission does. Tauri convention is to use `

` headings in markdown content for Tauri documentation generation purposes.", + "type": [ + "string", + "null" + ] + }, + "permissions": { + "description": "All permissions this set contains.", + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "PermissionSet": { + "description": "A set of direct permissions grouped together under a new name.", + "type": "object", + "required": [ + "description", + "identifier", + "permissions" + ], + "properties": { + "identifier": { + "description": "A unique identifier for the permission.", + "type": "string" + }, + "description": { + "description": "Human-readable description of what the permission does.", + "type": "string" + }, + "permissions": { + "description": "All permissions this set contains.", + "type": "array", + "items": { + "$ref": "#/definitions/PermissionKind" + } + } + } + }, + "Permission": { + "description": "Descriptions of explicit privileges of commands.\n\nIt can enable commands to be accessible in the frontend of the application.\n\nIf the scope is defined it can be used to fine grain control the access of individual or multiple commands.", + "type": "object", + "required": [ + "identifier" + ], + "properties": { + "version": { + "description": "The version of the permission.", + "type": [ + "integer", + "null" + ], + "format": "uint64", + "minimum": 1.0 + }, + "identifier": { + "description": "A unique identifier for the permission.", + "type": "string" + }, + "description": { + "description": "Human-readable description of what the permission does. Tauri internal convention is to use `

` headings in markdown content for Tauri documentation generation purposes.", + "type": [ + "string", + "null" + ] + }, + "commands": { + "description": "Allowed or denied commands when using this permission.", + "default": { + "allow": [], + "deny": [] + }, + "allOf": [ + { + "$ref": "#/definitions/Commands" + } + ] + }, + "scope": { + "description": "Allowed or denied scoped when using this permission.", + "allOf": [ + { + "$ref": "#/definitions/Scopes" + } + ] + }, + "platforms": { + "description": "Target platforms this permission applies. By default all platforms are affected by this permission.", + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/Target" + } + } + } + }, + "Commands": { + "description": "Allowed and denied commands inside a permission.\n\nIf two commands clash inside of `allow` and `deny`, it should be denied by default.", + "type": "object", + "properties": { + "allow": { + "description": "Allowed command.", + "default": [], + "type": "array", + "items": { + "type": "string" + } + }, + "deny": { + "description": "Denied command, which takes priority.", + "default": [], + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "Scopes": { + "description": "An argument for fine grained behavior control of Tauri commands.\n\nIt can be of any serde serializable type and is used to allow or prevent certain actions inside a Tauri command. The configured scope is passed to the command and will be enforced by the command implementation.\n\n## Example\n\n```json { \"allow\": [{ \"path\": \"$HOME/**\" }], \"deny\": [{ \"path\": \"$HOME/secret.txt\" }] } ```", + "type": "object", + "properties": { + "allow": { + "description": "Data that defines what is allowed by the scope.", + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/Value" + } + }, + "deny": { + "description": "Data that defines what is denied by the scope. This should be prioritized by validation logic.", + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/Value" + } + } + } + }, + "Value": { + "description": "All supported ACL values.", + "anyOf": [ + { + "description": "Represents a null JSON value.", + "type": "null" + }, + { + "description": "Represents a [`bool`].", + "type": "boolean" + }, + { + "description": "Represents a valid ACL [`Number`].", + "allOf": [ + { + "$ref": "#/definitions/Number" + } + ] + }, + { + "description": "Represents a [`String`].", + "type": "string" + }, + { + "description": "Represents a list of other [`Value`]s.", + "type": "array", + "items": { + "$ref": "#/definitions/Value" + } + }, + { + "description": "Represents a map of [`String`] keys to [`Value`]s.", + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/Value" + } + } + ] + }, + "Number": { + "description": "A valid ACL number.", + "anyOf": [ + { + "description": "Represents an [`i64`].", + "type": "integer", + "format": "int64" + }, + { + "description": "Represents a [`f64`].", + "type": "number", + "format": "double" + } + ] + }, + "Target": { + "description": "Platform target.", + "oneOf": [ + { + "description": "MacOS.", + "type": "string", + "enum": [ + "macOS" + ] + }, + { + "description": "Windows.", + "type": "string", + "enum": [ + "windows" + ] + }, + { + "description": "Linux.", + "type": "string", + "enum": [ + "linux" + ] + }, + { + "description": "Android.", + "type": "string", + "enum": [ + "android" + ] + }, + { + "description": "iOS.", + "type": "string", + "enum": [ + "iOS" + ] + } + ] + }, + "PermissionKind": { + "type": "string", + "oneOf": [ + { + "description": "Enables the is_registered command without any pre-configured scope.", + "type": "string", + "const": "allow-is-registered", + "markdownDescription": "Enables the is_registered command without any pre-configured scope." + }, + { + "description": "Denies the is_registered command without any pre-configured scope.", + "type": "string", + "const": "deny-is-registered", + "markdownDescription": "Denies the is_registered command without any pre-configured scope." + }, + { + "description": "Enables the register command without any pre-configured scope.", + "type": "string", + "const": "allow-register", + "markdownDescription": "Enables the register command without any pre-configured scope." + }, + { + "description": "Denies the register command without any pre-configured scope.", + "type": "string", + "const": "deny-register", + "markdownDescription": "Denies the register command without any pre-configured scope." + }, + { + "description": "Enables the register_all command without any pre-configured scope.", + "type": "string", + "const": "allow-register-all", + "markdownDescription": "Enables the register_all command without any pre-configured scope." + }, + { + "description": "Denies the register_all command without any pre-configured scope.", + "type": "string", + "const": "deny-register-all", + "markdownDescription": "Denies the register_all command without any pre-configured scope." + }, + { + "description": "Enables the unregister command without any pre-configured scope.", + "type": "string", + "const": "allow-unregister", + "markdownDescription": "Enables the unregister command without any pre-configured scope." + }, + { + "description": "Denies the unregister command without any pre-configured scope.", + "type": "string", + "const": "deny-unregister", + "markdownDescription": "Denies the unregister command without any pre-configured scope." + }, + { + "description": "Enables the unregister_all command without any pre-configured scope.", + "type": "string", + "const": "allow-unregister-all", + "markdownDescription": "Enables the unregister_all command without any pre-configured scope." + }, + { + "description": "Denies the unregister_all command without any pre-configured scope.", + "type": "string", + "const": "deny-unregister-all", + "markdownDescription": "Denies the unregister_all command without any pre-configured scope." + }, + { + "description": "No features are enabled by default, as we believe\nthe shortcuts can be inherently dangerous and it is\napplication specific if specific shortcuts should be\nregistered or unregistered.\n", + "type": "string", + "const": "default", + "markdownDescription": "No features are enabled by default, as we believe\nthe shortcuts can be inherently dangerous and it is\napplication specific if specific shortcuts should be\nregistered or unregistered.\n" + } + ] + } + } +} \ No newline at end of file diff --git a/plugins/global-shortcut/rollup.config.js b/plugins/global-shortcut/rollup.config.js new file mode 100644 index 0000000..1f349ec --- /dev/null +++ b/plugins/global-shortcut/rollup.config.js @@ -0,0 +1,7 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +import { createConfig } from '../../shared/rollup.config.js' + +export default createConfig() diff --git a/plugins/global-shortcut/src/error.rs b/plugins/global-shortcut/src/error.rs new file mode 100644 index 0000000..9b8e5cb --- /dev/null +++ b/plugins/global-shortcut/src/error.rs @@ -0,0 +1,44 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use serde::{Serialize, Serializer}; + +/// Errors that can happen while registering, unregistering or parsing global shortcuts. +#[derive(Debug, thiserror::Error)] +#[non_exhaustive] +pub enum Error { + /// An error returned by the underlying `global_hotkey` crate, for example when the OS + /// refuses to register or unregister a shortcut, or when a shortcut string fails to parse. + #[error("{0}")] + GlobalHotkey(String), + /// Failed to receive the result of an operation dispatched to the main thread because the + /// sending end of the channel was dropped before it could reply. + #[error(transparent)] + RecvError(#[from] std::sync::mpsc::RecvError), + /// An error returned by the Tauri runtime, for example when dispatching a closure to run on + /// the main thread fails. + #[error(transparent)] + Tauri(#[from] tauri::Error), +} + +impl Serialize for Error { + fn serialize(&self, serializer: S) -> std::result::Result + where + S: Serializer, + { + serializer.serialize_str(self.to_string().as_ref()) + } +} + +impl From for Error { + fn from(value: global_hotkey::Error) -> Self { + Self::GlobalHotkey(value.to_string()) + } +} + +impl From for Error { + fn from(value: global_hotkey::hotkey::HotKeyParseError) -> Self { + Self::GlobalHotkey(value.to_string()) + } +} diff --git a/plugins/global-shortcut/src/lib.rs b/plugins/global-shortcut/src/lib.rs new file mode 100644 index 0000000..19a4fa1 --- /dev/null +++ b/plugins/global-shortcut/src/lib.rs @@ -0,0 +1,497 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +//! Register global shortcuts. +//! +//! - Supported platforms: Windows, Linux and macOS. +//! - On macOS, registering and unregistering shortcuts must happen on the main thread, so every +//! [`GlobalShortcut`] operation dispatches to it and blocks until it completes. + +#![doc( + html_logo_url = "https://github.com/tauri-apps/tauri/raw/dev/app-icon.png", + html_favicon_url = "https://github.com/tauri-apps/tauri/raw/dev/app-icon.png" +)] +#![cfg(not(any(target_os = "android", target_os = "ios")))] + +use std::{ + collections::HashMap, + str::FromStr, + sync::{Arc, Mutex}, +}; + +use global_hotkey::GlobalHotKeyEvent; +pub use global_hotkey::{ + hotkey::{Code, HotKey as Shortcut, Modifiers}, + GlobalHotKeyEvent as ShortcutEvent, HotKeyState as ShortcutState, +}; +use serde::Serialize; +use tauri::{ + ipc::Channel, + plugin::{Builder as PluginBuilder, TauriPlugin}, + AppHandle, Manager, Runtime, State, +}; + +mod error; + +pub use error::Error; +type Result = std::result::Result; + +type HotKeyId = u32; +type HandlerFn = Box, &Shortcut, ShortcutEvent) + Send + Sync + 'static>; + +/// Internal wrapper around a [`Shortcut`] that lets the shortcut-accepting APIs on +/// [`GlobalShortcut`] and [`Builder`] be generic over an already-parsed [`Shortcut`] or a +/// string accelerator such as `"CmdOrControl+Q"`. +pub struct ShortcutWrapper(Shortcut); + +impl From for ShortcutWrapper { + fn from(value: Shortcut) -> Self { + Self(value) + } +} + +impl TryFrom<&str> for ShortcutWrapper { + type Error = global_hotkey::hotkey::HotKeyParseError; + fn try_from(value: &str) -> std::result::Result { + Shortcut::from_str(value).map(ShortcutWrapper) + } +} + +struct RegisteredShortcut { + shortcut: Shortcut, + handler: Option>>, +} + +struct GlobalHotKeyManager(global_hotkey::GlobalHotKeyManager); + +/// SAFETY: we ensure it is run on main thread only +unsafe impl Send for GlobalHotKeyManager {} +/// SAFETY: we ensure it is run on main thread only +unsafe impl Sync for GlobalHotKeyManager {} + +/// The global shortcut APIs, accessible through [`GlobalShortcutExt::global_shortcut`]. +/// +/// Registering, unregistering and checking a shortcut all dispatch the underlying +/// `global_hotkey` call to the app's main thread and block until it finishes, since macOS +/// requires the hotkey manager to run on the main thread. +pub struct GlobalShortcut { + #[allow(dead_code)] + app: AppHandle, + manager: Arc, + shortcuts: Arc>>>, +} + +macro_rules! run_main_thread { + ($handle:expr, $manager:expr, |$m:ident| $ex:expr) => {{ + let (tx, rx) = std::sync::mpsc::channel(); + let manager = $manager.clone(); + let task = move || { + let f = |$m: &GlobalHotKeyManager| $ex; + let _ = tx.send(f(&*manager)); + }; + $handle.run_on_main_thread(task)?; + rx.recv()? + }}; +} + +impl GlobalShortcut { + fn register_internal, &Shortcut, ShortcutEvent) + Send + Sync + 'static>( + &self, + shortcut: Shortcut, + handler: Option, + ) -> Result<()> { + let id = shortcut.id(); + let handler = handler.map(|h| Arc::new(Box::new(h) as HandlerFn)); + run_main_thread!(self.app, self.manager, |m| m.0.register(shortcut))?; + self.shortcuts + .lock() + .unwrap() + .insert(id, RegisteredShortcut { shortcut, handler }); + Ok(()) + } + + fn register_multiple_internal(&self, shortcuts: S, handler: Option) -> Result<()> + where + S: IntoIterator, + F: Fn(&AppHandle, &Shortcut, ShortcutEvent) + Send + Sync + 'static, + { + let handler = handler.map(|h| Arc::new(Box::new(h) as HandlerFn)); + + let hotkeys = shortcuts.into_iter().collect::>(); + + let mut shortcuts = self.shortcuts.lock().unwrap(); + for shortcut in hotkeys { + run_main_thread!(self.app, self.manager, |m| m.0.register(shortcut))?; + shortcuts.insert( + shortcut.id(), + RegisteredShortcut { + shortcut, + handler: handler.clone(), + }, + ); + } + + Ok(()) + } +} + +impl GlobalShortcut { + /// Register a shortcut. Returns an error if the shortcut is invalid or already registered + /// by this [`GlobalShortcut`] instance. + pub fn register(&self, shortcut: S) -> Result<()> + where + S: TryInto, + S::Error: std::error::Error, + { + self.register_internal( + try_into_shortcut(shortcut)?, + None::, &Shortcut, ShortcutEvent)>, + ) + } + + /// Register a shortcut with a handler that is called with the app handle, the triggered + /// [`Shortcut`] and the [`ShortcutEvent`] (whose `state` is [`ShortcutState::Pressed`] or + /// [`ShortcutState::Released`]) whenever the shortcut's key combination is pressed or + /// released. + pub fn on_shortcut(&self, shortcut: S, handler: F) -> Result<()> + where + S: TryInto, + S::Error: std::error::Error, + F: Fn(&AppHandle, &Shortcut, ShortcutEvent) + Send + Sync + 'static, + { + self.register_internal(try_into_shortcut(shortcut)?, Some(handler)) + } + + /// Register multiple shortcuts. + pub fn register_multiple(&self, shortcuts: S) -> Result<()> + where + S: IntoIterator, + T: TryInto, + T::Error: std::error::Error, + { + let mut s = Vec::new(); + for shortcut in shortcuts { + s.push(try_into_shortcut(shortcut)?); + } + self.register_multiple_internal(s, None::, &Shortcut, ShortcutEvent)>) + } + + /// Register multiple shortcuts with a handler. + pub fn on_shortcuts(&self, shortcuts: S, handler: F) -> Result<()> + where + S: IntoIterator, + T: TryInto, + T::Error: std::error::Error, + F: Fn(&AppHandle, &Shortcut, ShortcutEvent) + Send + Sync + 'static, + { + let mut s = Vec::new(); + for shortcut in shortcuts { + s.push(try_into_shortcut(shortcut)?); + } + self.register_multiple_internal(s, Some(handler)) + } + + /// Unregister a shortcut + pub fn unregister>(&self, shortcut: S) -> Result<()> + where + S::Error: std::error::Error, + { + let shortcut = try_into_shortcut(shortcut)?; + run_main_thread!(self.app, self.manager, |m| m.0.unregister(shortcut))?; + self.shortcuts.lock().unwrap().remove(&shortcut.id()); + Ok(()) + } + + /// Unregister multiple shortcuts. + pub fn unregister_multiple, S: IntoIterator>( + &self, + shortcuts: S, + ) -> Result<()> + where + T::Error: std::error::Error, + { + let mut mapped_shortcuts = Vec::new(); + for shortcut in shortcuts { + mapped_shortcuts.push(try_into_shortcut(shortcut)?); + } + + { + let mapped_shortcuts = mapped_shortcuts.clone(); + #[rustfmt::skip] + run_main_thread!(self.app, self.manager, |m| m.0.unregister_all(&mapped_shortcuts))?; + } + + let mut shortcuts = self.shortcuts.lock().unwrap(); + for s in mapped_shortcuts { + shortcuts.remove(&s.id()); + } + + Ok(()) + } + + /// Unregister all registered shortcuts. + pub fn unregister_all(&self) -> Result<()> { + let mut shortcuts = self.shortcuts.lock().unwrap(); + let hotkeys = std::mem::take(&mut *shortcuts); + let hotkeys = hotkeys.values().map(|s| s.shortcut).collect::>(); + #[rustfmt::skip] + let res = run_main_thread!(self.app, self.manager, |m| m.0.unregister_all(hotkeys.as_slice())); + res.map_err(Into::into) + } + + /// Determines whether the given shortcut is registered by this application or not. + /// + /// If the shortcut is registered by another application, it will still return `false`. + pub fn is_registered>(&self, shortcut: S) -> bool + where + S::Error: std::error::Error, + { + if let Ok(shortcut) = try_into_shortcut(shortcut) { + self.shortcuts.lock().unwrap().contains_key(&shortcut.id()) + } else { + false + } + } +} + +/// Extension trait for [`Manager`] implementors (e.g. [`AppHandle`]) that exposes access to the +/// global shortcut APIs. +/// +/// # Examples +/// +/// ```rust,no_run +/// use tauri::Runtime; +/// use tauri_plugin_global_shortcut::{GlobalShortcutExt, ShortcutState}; +/// +/// fn setup(app: &tauri::App) -> Result<(), Box> { +/// app.global_shortcut() +/// .on_shortcut("CommandOrControl+Shift+C", |_app, shortcut, event| { +/// if event.state == ShortcutState::Pressed { +/// println!("{shortcut:?} pressed"); +/// } +/// })?; +/// +/// Ok(()) +/// } +/// ``` +pub trait GlobalShortcutExt { + /// Returns the [`GlobalShortcut`] instance managed by this plugin. + fn global_shortcut(&self) -> &GlobalShortcut; +} + +impl> GlobalShortcutExt for T { + fn global_shortcut(&self) -> &GlobalShortcut { + self.state::>().inner() + } +} + +fn parse_shortcut>(shortcut: S) -> Result { + shortcut.as_ref().parse().map_err(Into::into) +} + +fn try_into_shortcut>(shortcut: S) -> Result +where + S::Error: std::error::Error, +{ + shortcut + .try_into() + .map(|s| s.0) + .map_err(|e| Error::GlobalHotkey(e.to_string())) +} + +#[derive(Clone, Serialize)] +struct ShortcutJsEvent { + shortcut: String, + id: u32, + state: ShortcutState, +} + +#[tauri::command] +fn register( + _app: AppHandle, + global_shortcut: State<'_, GlobalShortcut>, + shortcuts: Vec, + handler: Channel, +) -> Result<()> { + let mut hotkeys = Vec::new(); + + let mut shortcut_map = HashMap::new(); + for shortcut in shortcuts { + let hotkey = parse_shortcut(&shortcut)?; + shortcut_map.insert(hotkey.id(), shortcut); + hotkeys.push(hotkey); + } + + global_shortcut.register_multiple_internal( + hotkeys, + Some( + move |_app: &AppHandle, shortcut: &Shortcut, e: ShortcutEvent| { + let js_event = ShortcutJsEvent { + id: e.id, + state: e.state, + shortcut: shortcut.into_string(), + }; + let _ = handler.send(js_event); + }, + ), + ) +} + +#[tauri::command] +fn unregister( + _app: AppHandle, + global_shortcut: State<'_, GlobalShortcut>, + shortcuts: Vec, +) -> Result<()> { + let mut hotkeys = Vec::new(); + for shortcut in shortcuts { + hotkeys.push(parse_shortcut(&shortcut)?); + } + global_shortcut.unregister_multiple(hotkeys) +} + +#[tauri::command] +fn unregister_all( + _app: AppHandle, + global_shortcut: State<'_, GlobalShortcut>, +) -> Result<()> { + global_shortcut.unregister_all() +} + +#[tauri::command] +fn is_registered( + _app: AppHandle, + global_shortcut: State<'_, GlobalShortcut>, + shortcut: String, +) -> Result { + Ok(global_shortcut.is_registered(parse_shortcut(shortcut)?)) +} + +/// Builder for the global shortcut plugin, letting you configure shortcuts and a handler that +/// are registered as soon as the plugin is set up, before [`Builder::build`] is called. +/// +/// # Examples +/// +/// ```rust,no_run +/// use tauri::Runtime; +/// +/// fn setup(builder: tauri::Builder) -> tauri::Builder { +/// builder.plugin( +/// tauri_plugin_global_shortcut::Builder::new() +/// .with_shortcut("CommandOrControl+Shift+C") +/// .unwrap() +/// .with_handler(|_app, shortcut, event| { +/// println!("{shortcut:?}: {event:?}"); +/// }) +/// .build(), +/// ) +/// } +/// ``` +pub struct Builder { + shortcuts: Vec, + handler: Option>, +} + +impl Default for Builder { + fn default() -> Self { + Self { + shortcuts: Vec::new(), + handler: Default::default(), + } + } +} + +impl Builder { + /// Creates a new [`Builder`] with no shortcuts or handler configured. + pub fn new() -> Self { + Self::default() + } + + /// Add a shortcut to be registerd. + pub fn with_shortcut(mut self, shortcut: T) -> Result + where + T: TryInto, + T::Error: std::error::Error, + { + self.shortcuts.push(try_into_shortcut(shortcut)?); + Ok(self) + } + + /// Add multiple shortcuts to be registerd. + pub fn with_shortcuts(mut self, shortcuts: S) -> Result + where + S: IntoIterator, + T: TryInto, + T::Error: std::error::Error, + { + for shortcut in shortcuts { + self.shortcuts.push(try_into_shortcut(shortcut)?); + } + + Ok(self) + } + + /// Specify a global shortcut handler that will be triggered for any and all shortcuts. + pub fn with_handler, &Shortcut, ShortcutEvent) + Send + Sync + 'static>( + mut self, + handler: F, + ) -> Self { + self.handler.replace(Box::new(handler)); + self + } + + /// Builds the plugin. Shortcuts configured with [`Builder::with_shortcut`] / + /// [`Builder::with_shortcuts`] are registered when the plugin is set up, and the handler + /// configured with [`Builder::with_handler`] (if any) is called for every hotkey event, in + /// addition to any handler passed to [`GlobalShortcut::on_shortcut`] / + /// [`GlobalShortcut::on_shortcuts`] for that specific shortcut. + pub fn build(self) -> TauriPlugin { + let handler = self.handler; + let shortcuts = self.shortcuts; + PluginBuilder::new("global-shortcut") + .invoke_handler(tauri::generate_handler![ + register, + unregister, + unregister_all, + is_registered, + ]) + .setup(move |app, _api| { + let manager = global_hotkey::GlobalHotKeyManager::new()?; + let mut store = HashMap::>::new(); + for shortcut in shortcuts { + manager.register(shortcut)?; + store.insert( + shortcut.id(), + RegisteredShortcut { + shortcut, + handler: None, + }, + ); + } + + let shortcuts = Arc::new(Mutex::new(store)); + let shortcuts_ = shortcuts.clone(); + + let app_handle = app.clone(); + GlobalHotKeyEvent::set_event_handler(Some(move |e: GlobalHotKeyEvent| { + if let Some(shortcut) = shortcuts_.lock().unwrap().get(&e.id) { + if let Some(handler) = &shortcut.handler { + handler(&app_handle, &shortcut.shortcut, e); + } + if let Some(handler) = &handler { + handler(&app_handle, &shortcut.shortcut, e); + } + } + })); + + app.manage(GlobalShortcut { + app: app.clone(), + manager: Arc::new(GlobalHotKeyManager(manager)), + shortcuts, + }); + Ok(()) + }) + .build() + } +} diff --git a/plugins/global-shortcut/tsconfig.json b/plugins/global-shortcut/tsconfig.json new file mode 100644 index 0000000..4374468 --- /dev/null +++ b/plugins/global-shortcut/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "rootDir": "guest-js" + }, + "include": ["guest-js/*.ts"] +} diff --git a/plugins/haptics/CHANGELOG.md b/plugins/haptics/CHANGELOG.md new file mode 100644 index 0000000..e83e3ef --- /dev/null +++ b/plugins/haptics/CHANGELOG.md @@ -0,0 +1,70 @@ +# Changelog + +## [2.3.3] + +- [`db9c5998`](https://github.com/tauri-apps/plugins-workspace/commit/db9c5998feff9384f9cbbefcbe0d45937c00a1fc) ([#3531](https://github.com/tauri-apps/plugins-workspace/pull/3531) by [@Legend-Master](https://github.com/tauri-apps/plugins-workspace/../../Legend-Master)) Fix missing `consumer-rules.pro` on Gradle v9. +- [`f8053e65`](https://github.com/tauri-apps/plugins-workspace/commit/f8053e659e4ccd85c1f52833411ff8417cbc5e69) ([#3527](https://github.com/tauri-apps/plugins-workspace/pull/3527) by [@Legend-Master](https://github.com/tauri-apps/plugins-workspace/../../Legend-Master)) Documented Cargo feature flags in each plugin's crate-level documentation. + +## \[2.3.2] + +- [`93426f85`](https://github.com/tauri-apps/plugins-workspace/commit/93426f85120f49beb9f40222bff45185a32d54a9) Fixed an issue that caused docs.rs builds to fail. No user facing changes. + +## \[2.3.1] + +- [`6c9b61fb`](https://github.com/tauri-apps/plugins-workspace/commit/6c9b61fb658145d13893626112fc489f7458aa17) ([#3039](https://github.com/tauri-apps/plugins-workspace/pull/3039) by [@FabianLars](https://github.com/tauri-apps/plugins-workspace/../../FabianLars)) On Android, updated compileSdk to 36. + +## \[2.3.0] + +- [`f209b2f2`](https://github.com/tauri-apps/plugins-workspace/commit/f209b2f23cb29133c97ad5961fb46ef794dbe063) ([#2804](https://github.com/tauri-apps/plugins-workspace/pull/2804) by [@renovate](https://github.com/tauri-apps/plugins-workspace/../../renovate)) Updated tauri to 2.6 + +## \[2.2.5] + +- [`f634e524`](https://github.com/tauri-apps/plugins-workspace/commit/f634e5248ebe428f8305a59f74c13fc15147fb8e) This is an "empty" release to update the plugins' source files on crates.io and docs.rs. This should fix docs.rs build failures for projects using tauri plugins as dependencies. + +## \[2.2.4] + +- [`a1b3fa27`](https://github.com/tauri-apps/plugins-workspace/commit/a1b3fa27f11022c9b6622b4fab12d93239eb05de) ([#2515](https://github.com/tauri-apps/plugins-workspace/pull/2515) by [@FabianLars](https://github.com/tauri-apps/plugins-workspace/../../FabianLars)) Re-exported the `Geolocation`, `Haptics`, `Notification`, and `Os` structs so that they show up on docs.rs. + +## \[2.2.3] + +- [`406e6f48`](https://github.com/tauri-apps/plugins-workspace/commit/406e6f484cdc13d35c50fb949f7489ca9eeccc44) ([#2323](https://github.com/tauri-apps/plugins-workspace/pull/2323) by [@FabianLars](https://github.com/tauri-apps/plugins-workspace/../../FabianLars)) Fixed an issue that caused build failures when the `haptics` or `geolocation` plugin was used without their `specta` feature flag enabled. + +## \[2.2.2] + +- [`c9c13a0f`](https://github.com/tauri-apps/plugins-workspace/commit/c9c13a0fe7cdaac223843f5ba33176252f8e22f5) ([#2316](https://github.com/tauri-apps/plugins-workspace/pull/2316) by [@FabianLars](https://github.com/tauri-apps/plugins-workspace/../../FabianLars)) **Breaking change:** `specta` integration is now behind a `specta` feature flag like in Tauri. +- [`c9c13a0f`](https://github.com/tauri-apps/plugins-workspace/commit/c9c13a0fe7cdaac223843f5ba33176252f8e22f5) ([#2316](https://github.com/tauri-apps/plugins-workspace/pull/2316) by [@FabianLars](https://github.com/tauri-apps/plugins-workspace/../../FabianLars)) Unlock and widen `specta` version range to match Tauri. No API changes. + +## \[2.2.1] + +- [`fb67ab2b`](https://github.com/tauri-apps/plugins-workspace/commit/fb67ab2b926502bfc20d6b43fbdd156691ea8526) ([#2281](https://github.com/tauri-apps/plugins-workspace/pull/2281) by [@FabianLars](https://github.com/tauri-apps/plugins-workspace/../../FabianLars)) Added `specta-util` to fix a "dependency not found" compilation error. + +## \[2.2.0] + +- [`3a79266b`](https://github.com/tauri-apps/plugins-workspace/commit/3a79266b8cf96a55b1ae6339d725567d45a44b1d) ([#2173](https://github.com/tauri-apps/plugins-workspace/pull/2173) by [@FabianLars](https://github.com/tauri-apps/plugins-workspace/../../FabianLars)) Bumped all plugins to `v2.2.0`. From now, the versions for the Rust and JavaScript packages of each plugin will be in sync with each other. + +## \[2.0.1] + +- [`a1a82208`](https://github.com/tauri-apps/plugins-workspace/commit/a1a82208ed4ab87f83310be0dc95428aec9ab241) ([#1873](https://github.com/tauri-apps/plugins-workspace/pull/1873) by [@lucasfernog](https://github.com/tauri-apps/plugins-workspace/../../lucasfernog)) Downgrade MSRV to 1.77.2 to support Windows 7. + +## \[2.0.0] + +- [`e2c4dfb6`](https://github.com/tauri-apps/plugins-workspace/commit/e2c4dfb6af43e5dd8d9ceba232c315f5febd55c1) Update to tauri v2 stable release. + +## \[2.0.0-rc.1] + +- [`e2e97db5`](https://github.com/tauri-apps/plugins-workspace/commit/e2e97db51983267f5be84d4f6f0278d58834d1f5) ([#1701](https://github.com/tauri-apps/plugins-workspace/pull/1701) by [@lucasfernog](https://github.com/tauri-apps/plugins-workspace/../../lucasfernog)) Update to tauri 2.0.0-rc.8 + +## \[2.0.0-rc.2] + +- [`b9147758`](https://github.com/tauri-apps/plugins-workspace/commit/b914775898c2bee7ceb20bd17ee595005cd17a64) ([#1679](https://github.com/tauri-apps/plugins-workspace/pull/1679) by [@lucasfernog](https://github.com/tauri-apps/plugins-workspace/../../lucasfernog)) Explicitly set a minimum macOS version for the Swift package. + +## \[2.0.0-rc.1] + +### changes + +- [`6b079cfd`](https://github.com/tauri-apps/plugins-workspace/commit/6b079cfdd107c94abc2c7300f6af00bac3ff4040) ([#1649](https://github.com/tauri-apps/plugins-workspace/pull/1649) by [@ahqsoftwares](https://github.com/tauri-apps/plugins-workspace/../../ahqsoftwares)) Remove targetSdk from build.kts files as it is deprecated and will be removed from DSL v9.0 + +## \[2.0.0-rc.0] + +- [`9606089b`](https://github.com/tauri-apps/plugins-workspace/commit/9606089b2add4a17f80ed5a09d59ce94824bd672) ([#1599](https://github.com/tauri-apps/plugins-workspace/pull/1599)) Initial release. +- [`9887d1`](https://github.com/tauri-apps/plugins-workspace/commit/9887d14bd0e971c4c0f5c1188fc4005d3fc2e29e) Update to tauri RC. diff --git a/plugins/haptics/Cargo.toml b/plugins/haptics/Cargo.toml new file mode 100644 index 0000000..524a446 --- /dev/null +++ b/plugins/haptics/Cargo.toml @@ -0,0 +1,37 @@ +[package] +name = "tauri-plugin-haptics" +description = "Haptic feedback and vibrations on Android and iOS" +version = "2.3.3" +edition = { workspace = true } +authors = { workspace = true } +license = { workspace = true } +repository = { workspace = true } +links = "tauri-plugin-haptics" +exclude = ["/guest-js", "/package.json", "/rollup.config.js", "/tsconfig.json"] + +[package.metadata.docs.rs] +targets = ["x86_64-linux-android"] + +[package.metadata.platforms.support] +windows = { level = "none", notes = "" } +linux = { level = "none", notes = "" } +macos = { level = "none", notes = "" } +android = { level = "full", notes = "" } +ios = { level = "full", notes = "" } + +[build-dependencies] +tauri-plugin = { workspace = true, features = ["build"] } + +[dependencies] +serde = { workspace = true } +serde_json = { workspace = true } +tauri = { workspace = true } +log = { workspace = true } +thiserror = { workspace = true } +specta = { workspace = true, optional = true } + +[target.'cfg(target_os = "ios")'.dependencies] +tauri = { workspace = true, features = ["wry"] } + +[features] +specta = ["dep:specta", "tauri/specta"] diff --git a/plugins/haptics/LICENSE.spdx b/plugins/haptics/LICENSE.spdx new file mode 100644 index 0000000..cdd0df5 --- /dev/null +++ b/plugins/haptics/LICENSE.spdx @@ -0,0 +1,20 @@ +SPDXVersion: SPDX-2.1 +DataLicense: CC0-1.0 +PackageName: tauri +DataFormat: SPDXRef-1 +PackageSupplier: Organization: The Tauri Programme in the Commons Conservancy +PackageHomePage: https://tauri.app +PackageLicenseDeclared: Apache-2.0 +PackageLicenseDeclared: MIT +PackageCopyrightText: 2019-2022, The Tauri Programme in the Commons Conservancy +PackageSummary: Tauri is a rust project that enables developers to make secure +and small desktop applications using a web frontend. + +PackageComment: The package includes the following libraries; see +Relationship information. + +Created: 2019-05-20T09:00:00Z +PackageDownloadLocation: git://github.com/tauri-apps/tauri +PackageDownloadLocation: git+https://github.com/tauri-apps/tauri.git +PackageDownloadLocation: git+ssh://github.com/tauri-apps/tauri.git +Creator: Person: Daniel Thompson-Yvetot \ No newline at end of file diff --git a/plugins/haptics/LICENSE_APACHE-2.0 b/plugins/haptics/LICENSE_APACHE-2.0 new file mode 100644 index 0000000..4947287 --- /dev/null +++ b/plugins/haptics/LICENSE_APACHE-2.0 @@ -0,0 +1,177 @@ + + 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 \ No newline at end of file diff --git a/plugins/haptics/LICENSE_MIT b/plugins/haptics/LICENSE_MIT new file mode 100644 index 0000000..4d75472 --- /dev/null +++ b/plugins/haptics/LICENSE_MIT @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2017 - Present Tauri Apps 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. \ No newline at end of file diff --git a/plugins/haptics/README.md b/plugins/haptics/README.md new file mode 100644 index 0000000..8a7cd84 --- /dev/null +++ b/plugins/haptics/README.md @@ -0,0 +1,135 @@ +![haptics](https://github.com/tauri-apps/plugins-workspace/raw/v2/plugins/haptics/banner.png) + +Haptic feedback and vibrations on Android and iOS. + +There are no standards/requirements for vibration support on Android, so the `feedback` APIs may not work correctly on more affordable phones, including recently released ones. + +| Platform | Supported | +| -------- | --------- | +| Linux | x | +| Windows | x | +| macOS | x | +| Android | ✓ | +| iOS | ✓ | + +## Install + +_This plugin requires a Rust version of at least **1.77.2**_ + +There are three general methods of installation that we can recommend. + +1. Use crates.io and npm (easiest, and requires you to trust that our publishing pipeline worked) +2. Pull sources directly from Github using git tags / revision hashes (most secure) +3. Git submodule install this repo in your tauri project and then use file protocol to ingest the source (most secure, but inconvenient to use) + +Install the Core plugin by adding the following to your `Cargo.toml` file: + +`src-tauri/Cargo.toml` + +```toml +[dependencies] +tauri-plugin-haptics = "2.0.0" +# alternatively with Git: +tauri-plugin-haptics = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v2" } +``` + +You can install the JavaScript Guest bindings using your preferred JavaScript package manager: + + + +```sh +pnpm add @tauri-apps/plugin-haptics +# or +npm add @tauri-apps/plugin-haptics +# or +yarn add @tauri-apps/plugin-haptics +``` + +## Usage + +First you need to register the core plugin with Tauri: + +`src-tauri/src/lib.rs` + +```rust +fn main() { + tauri::Builder::default() + .plugin(tauri_plugin_haptics::init()) + .run(tauri::generate_context!()) + .expect("error while running tauri application"); +} +``` + +Second, add the required permissions in the project: + +`src-tauri/capabilities/default.json` + +```json + "permissions": [ + "haptics:allow-impact-feedback", + "haptics:allow-notification-feedback", + "haptics:allow-selection-feedback", + "haptics:allow-vibrate" + ] +``` + +Afterwards all the plugin's APIs are available through the JavaScript guest bindings: + +```javascript +import { + vibrate, + impactFeedback, + notificationFeedback, + selectionFeedback +} from '@tauri-apps/plugin-haptics' + +await vibrate(1) +await impactFeedback('medium') +await notificationFeedback('warning') +await selectionFeedback() +``` + +## Contributing + +PRs accepted. Please make sure to read the Contributing Guide before making a pull request. + +## Contributed By + + + + + + + + +
+ + CrabNebula + + + + Rescue.co + +
+ +## Partners + + + + + + + +
+ + CrabNebula + +
+ +For the complete list of sponsors please visit our [website](https://tauri.app#sponsors) and [Open Collective](https://opencollective.com/tauri). + +## License + +Code: (c) 2015 - Present - The Tauri Programme within The Commons Conservancy. + +MIT or MIT/Apache 2.0 where applicable. diff --git a/plugins/haptics/SECURITY.md b/plugins/haptics/SECURITY.md new file mode 100644 index 0000000..4f09bba --- /dev/null +++ b/plugins/haptics/SECURITY.md @@ -0,0 +1,23 @@ +# Security Policy + +**Do not report security vulnerabilities through public GitHub issues.** + +**Please use the [Private Vulnerability Disclosure](https://docs.github.com/en/code-security/security-advisories/guidance-on-reporting-and-writing-information-about-vulnerabilities/privately-reporting-a-security-vulnerability#privately-reporting-a-security-vulnerability) feature of GitHub.** + +Include as much of the following information: + +- Type of issue (e.g. improper input parsing, privilege escalation, etc.) +- The location of the affected source code (tag/branch/commit or direct URL) +- Any special configuration required to reproduce the issue +- The distribution affected or used to help us with reproduction of the issue +- Step-by-step instructions to reproduce the issue +- Ideally a reproduction repository +- Impact of the issue, including how an attacker might exploit the issue + +We prefer to receive reports in English. + +## Contact + +Please disclose a vulnerability or security relevant issue here: [https://github.com/tauri-apps/plugins-workspace/security/advisories/new](https://github.com/tauri-apps/plugins-workspace/security/advisories/new). + +Alternatively, you can also contact us by email via [security@tauri.app](mailto:security@tauri.app). diff --git a/plugins/haptics/android/.gitignore b/plugins/haptics/android/.gitignore new file mode 100644 index 0000000..c0f21ec --- /dev/null +++ b/plugins/haptics/android/.gitignore @@ -0,0 +1,2 @@ +/build +/.tauri diff --git a/plugins/haptics/android/build.gradle.kts b/plugins/haptics/android/build.gradle.kts new file mode 100644 index 0000000..a178826 --- /dev/null +++ b/plugins/haptics/android/build.gradle.kts @@ -0,0 +1,36 @@ +plugins { + id("com.android.library") + id("org.jetbrains.kotlin.android") +} + +android { + namespace = "app.tauri.haptics" + compileSdk = 36 + + defaultConfig { + minSdk = 24 + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + consumerProguardFiles("consumer-rules.pro") + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 + } + kotlinOptions { + jvmTarget = "1.8" + } +} + +dependencies { + + implementation("androidx.core:core-ktx:1.9.0") + implementation("androidx.appcompat:appcompat:1.6.0") + implementation("com.google.android.material:material:1.7.0") + implementation("com.fasterxml.jackson.core:jackson-databind:2.15.3") + testImplementation("junit:junit:4.13.2") + androidTestImplementation("androidx.test.ext:junit:1.1.5") + androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1") + implementation(project(":tauri-android")) +} diff --git a/plugins/haptics/android/consumer-rules.pro b/plugins/haptics/android/consumer-rules.pro new file mode 100644 index 0000000..481bb43 --- /dev/null +++ b/plugins/haptics/android/consumer-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/plugins/haptics/android/settings.gradle b/plugins/haptics/android/settings.gradle new file mode 100644 index 0000000..14a752e --- /dev/null +++ b/plugins/haptics/android/settings.gradle @@ -0,0 +1,2 @@ +include ':tauri-android' +project(':tauri-android').projectDir = new File('./.tauri/tauri-api') diff --git a/plugins/haptics/android/src/androidTest/java/ExampleInstrumentedTest.kt b/plugins/haptics/android/src/androidTest/java/ExampleInstrumentedTest.kt new file mode 100644 index 0000000..1b408ac --- /dev/null +++ b/plugins/haptics/android/src/androidTest/java/ExampleInstrumentedTest.kt @@ -0,0 +1,28 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +package app.tauri.haptics + +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.ext.junit.runners.AndroidJUnit4 + +import org.junit.Test +import org.junit.runner.RunWith + +import org.junit.Assert.* + +/** + * Instrumented test, which will execute on an Android device. + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +@RunWith(AndroidJUnit4::class) +class ExampleInstrumentedTest { + @Test + fun useAppContext() { + // Context of the app under test. + val appContext = InstrumentationRegistry.getInstrumentation().targetContext + assertEquals("app.tauri.haptics", appContext.packageName) + } +} diff --git a/plugins/haptics/android/src/main/AndroidManifest.xml b/plugins/haptics/android/src/main/AndroidManifest.xml new file mode 100644 index 0000000..042e61a --- /dev/null +++ b/plugins/haptics/android/src/main/AndroidManifest.xml @@ -0,0 +1,4 @@ + + + + diff --git a/plugins/haptics/android/src/main/java/HapticsPlugin.kt b/plugins/haptics/android/src/main/java/HapticsPlugin.kt new file mode 100644 index 0000000..5a6bf1b --- /dev/null +++ b/plugins/haptics/android/src/main/java/HapticsPlugin.kt @@ -0,0 +1,143 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +package app.tauri.haptics + +import android.app.Activity +import android.content.Context +import android.os.Build +import android.os.VibrationEffect +import android.os.Vibrator +import android.os.VibratorManager +import app.tauri.Logger +import app.tauri.annotation.Command +import app.tauri.annotation.InvokeArg +import app.tauri.annotation.TauriPlugin +import app.tauri.haptics.patterns.ImpactPatternHeavy +import app.tauri.haptics.patterns.ImpactPatternLight +import app.tauri.haptics.patterns.ImpactPatternMedium +import app.tauri.haptics.patterns.ImpactPatternRigid +import app.tauri.haptics.patterns.ImpactPatternSoft +import app.tauri.haptics.patterns.NotificationPatternError +import app.tauri.haptics.patterns.NotificationPatternSuccess +import app.tauri.haptics.patterns.NotificationPatternWarning +import app.tauri.haptics.patterns.Pattern +import app.tauri.haptics.patterns.SelectionPattern +import app.tauri.plugin.Invoke +import app.tauri.plugin.Plugin +import com.fasterxml.jackson.annotation.JsonProperty + +@InvokeArg +class HapticsOptions { + var duration: Long = 300 +} + +@InvokeArg +class NotificationFeedbackArgs { + val type: NotificationFeedbackType = NotificationFeedbackType.Success +} + +@InvokeArg +enum class NotificationFeedbackType { + @JsonProperty("success") + Success, + @JsonProperty("warning") + Warning, + @JsonProperty("error") + Error; + + fun into(): Pattern { + return when(this) { + Success -> NotificationPatternSuccess + Warning -> NotificationPatternWarning + Error -> NotificationPatternError + } + } +} + +@InvokeArg +class ImpactFeedbackArgs { + val style: ImpactFeedbackStyle = ImpactFeedbackStyle.Medium +} + +@InvokeArg +enum class ImpactFeedbackStyle { + @JsonProperty("light") + Light, + @JsonProperty("medium") + Medium, + @JsonProperty("heavy") + Heavy, + @JsonProperty("soft") + Soft, + @JsonProperty("rigid") + Rigid; + + fun into(): Pattern { + return when(this) { + Light -> ImpactPatternLight + Medium -> ImpactPatternMedium + Heavy -> ImpactPatternHeavy + Soft -> ImpactPatternSoft + Rigid -> ImpactPatternRigid + } + } +} + +@TauriPlugin +class HapticsPlugin(private val activity: Activity): Plugin(activity) { + private val vibrator: Vibrator = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + val vibManager = activity.applicationContext.getSystemService(Context.VIBRATOR_MANAGER_SERVICE) as VibratorManager + vibManager.defaultVibrator + } else { + @Suppress("DEPRECATION") + activity.applicationContext.getSystemService(Context.VIBRATOR_SERVICE) as Vibrator + } + + // + // TAURI COMMANDS + // + + @Command + fun vibrate(invoke: Invoke) { + val args = invoke.parseArgs(HapticsOptions::class.java) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + vibrator.vibrate(VibrationEffect.createOneShot(args.duration, VibrationEffect.DEFAULT_AMPLITUDE)) + } else { + vibrator.vibrate(args.duration) + } + invoke.resolve() + } + + @Command + fun impactFeedback(invoke: Invoke) { + val args = invoke.parseArgs(ImpactFeedbackArgs::class.java) + vibratePattern(args.style.into()) + invoke.resolve() + } + + @Command + fun notificationFeedback(invoke: Invoke) { + val args = invoke.parseArgs(NotificationFeedbackArgs::class.java) + vibratePattern(args.type.into()) + invoke.resolve() + } + + // TODO: Consider breaking this up into Start,Change,End like capacitor + @Command + fun selectionFeedback(invoke: Invoke) { + vibratePattern(SelectionPattern) + invoke.resolve() + } + + // INTERNAL FUNCTIONS + + private fun vibratePattern(pattern: Pattern) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + vibrator.vibrate(VibrationEffect.createWaveform(pattern.timings, pattern.amplitudes, -1)) + } else { + vibrator.vibrate(pattern.oldSDKPattern, -1) + } + } +} diff --git a/plugins/haptics/android/src/main/java/patterns/Impact.kt b/plugins/haptics/android/src/main/java/patterns/Impact.kt new file mode 100644 index 0000000..d40e750 --- /dev/null +++ b/plugins/haptics/android/src/main/java/patterns/Impact.kt @@ -0,0 +1,35 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +package app.tauri.haptics.patterns + +val ImpactPatternLight = Pattern( + longArrayOf(0, 50), + intArrayOf(0, 30), + longArrayOf(0, 20) +) + +val ImpactPatternMedium = Pattern( + longArrayOf(0, 43), + intArrayOf(0, 50), + longArrayOf(0, 43) +) + +val ImpactPatternHeavy = Pattern( + longArrayOf(0, 60), + intArrayOf(0, 70), + longArrayOf(0, 61) +) + +val ImpactPatternSoft = Pattern( + longArrayOf(0, 50), + intArrayOf(0, 30), + longArrayOf(0, 20) +) + +val ImpactPatternRigid = Pattern( + longArrayOf(0, 43), + intArrayOf(0, 50), + longArrayOf(0, 43) +) \ No newline at end of file diff --git a/plugins/haptics/android/src/main/java/patterns/Notification.kt b/plugins/haptics/android/src/main/java/patterns/Notification.kt new file mode 100644 index 0000000..2f70c23 --- /dev/null +++ b/plugins/haptics/android/src/main/java/patterns/Notification.kt @@ -0,0 +1,23 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +package app.tauri.haptics.patterns + +val NotificationPatternSuccess = Pattern( + longArrayOf(0, 40, 100, 40), + intArrayOf(0, 50, 0, 60), + longArrayOf(0, 40, 100, 40) +) + +val NotificationPatternWarning = Pattern( + longArrayOf(0, 40, 120, 60), + intArrayOf(0, 40, 0, 60), + longArrayOf(0, 40, 120, 60) +) + +val NotificationPatternError = Pattern( + longArrayOf(0, 60, 100, 40, 80, 50), + intArrayOf(0, 50, 0, 40, 0, 50), + longArrayOf(0, 60, 100, 40, 80, 50) +) diff --git a/plugins/haptics/android/src/main/java/patterns/Pattern.kt b/plugins/haptics/android/src/main/java/patterns/Pattern.kt new file mode 100644 index 0000000..5f99b02 --- /dev/null +++ b/plugins/haptics/android/src/main/java/patterns/Pattern.kt @@ -0,0 +1,11 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +package app.tauri.haptics.patterns + +class Pattern ( + val timings: LongArray, + val amplitudes: IntArray, + val oldSDKPattern: LongArray +) {} \ No newline at end of file diff --git a/plugins/haptics/android/src/main/java/patterns/Selection.kt b/plugins/haptics/android/src/main/java/patterns/Selection.kt new file mode 100644 index 0000000..02dce4c --- /dev/null +++ b/plugins/haptics/android/src/main/java/patterns/Selection.kt @@ -0,0 +1,11 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +package app.tauri.haptics.patterns + +val SelectionPattern = Pattern ( + timings = longArrayOf(0, 50), + amplitudes = intArrayOf(0, 30), + oldSDKPattern = longArrayOf(0, 70) +) \ No newline at end of file diff --git a/plugins/haptics/android/src/test/java/ExampleUnitTest.kt b/plugins/haptics/android/src/test/java/ExampleUnitTest.kt new file mode 100644 index 0000000..562188f --- /dev/null +++ b/plugins/haptics/android/src/test/java/ExampleUnitTest.kt @@ -0,0 +1,21 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +package app.tauri.haptics + +import org.junit.Test + +import org.junit.Assert.* + +/** + * Example local unit test, which will execute on the development machine (host). + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +class ExampleUnitTest { + @Test + fun addition_isCorrect() { + assertEquals(4, 2 + 2) + } +} diff --git a/plugins/haptics/api-iife.js b/plugins/haptics/api-iife.js new file mode 100644 index 0000000..0f50b70 --- /dev/null +++ b/plugins/haptics/api-iife.js @@ -0,0 +1 @@ +if("__TAURI__"in window){var __TAURI_PLUGIN_HAPTICS__=function(t,r){"use strict";const e={async vibrate(t){try{return{status:"ok",data:await r.invoke("plugin:haptics|vibrate",{duration:t})}}catch(t){if(t instanceof Error)throw t;return{status:"error",error:t}}},async impactFeedback(t){try{return{status:"ok",data:await r.invoke("plugin:haptics|impact_feedback",{style:t})}}catch(t){if(t instanceof Error)throw t;return{status:"error",error:t}}},async notificationFeedback(t){try{return{status:"ok",data:await r.invoke("plugin:haptics|notification_feedback",{type:t})}}catch(t){if(t instanceof Error)throw t;return{status:"error",error:t}}},async selectionFeedback(){try{return{status:"ok",data:await r.invoke("plugin:haptics|selection_feedback")}}catch(t){if(t instanceof Error)throw t;return{status:"error",error:t}}}};return t.impactFeedback=async function(t){return e.impactFeedback(t)},t.notificationFeedback=async function(t){return e.notificationFeedback(t)},t.selectionFeedback=async function(){return e.selectionFeedback()},t.vibrate=async function(t){return e.vibrate(t)},t}({},window.__TAURI__.core);Object.defineProperty(window.__TAURI__,"haptics",{value:__TAURI_PLUGIN_HAPTICS__})} diff --git a/plugins/haptics/build.rs b/plugins/haptics/build.rs new file mode 100644 index 0000000..2556cb6 --- /dev/null +++ b/plugins/haptics/build.rs @@ -0,0 +1,23 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +const COMMANDS: &[&str] = &[ + "vibrate", + "impact_feedback", + "notification_feedback", + "selection_feedback", +]; + +fn main() { + let result = tauri_plugin::Builder::new(COMMANDS) + .global_api_script_path("./api-iife.js") + .android_path("android") + .ios_path("ios") + .try_build(); + + // when building documentation for Android the plugin build result is always Err() and is irrelevant to the crate documentation build + if !(cfg!(docsrs) && std::env::var("TARGET").unwrap().contains("android")) { + result.unwrap(); + } +} diff --git a/plugins/haptics/contributors/crabnebula.svg b/plugins/haptics/contributors/crabnebula.svg new file mode 100644 index 0000000..40e2413 --- /dev/null +++ b/plugins/haptics/contributors/crabnebula.svg @@ -0,0 +1,31 @@ + \ No newline at end of file diff --git a/plugins/haptics/contributors/rescue.png b/plugins/haptics/contributors/rescue.png new file mode 100644 index 0000000000000000000000000000000000000000..2b5916f408daacf0ae1938d2e9dc7511ceffdbbf GIT binary patch literal 8071 zcmZ8`by$?$7cHIA0@B?KAt6Xf_keT`ozguZAT3=2qI5`i!_W=VC5|Rym9w_|qsK7HX?^PaSpwI*0Zyvs?|Bd0R$OB6a9tb@K zkF5V-4<)$Tqt0Lbza=%eCR`By2tTmktD4Zgy2!oKqlbs>zh}V>Jis0;!IKYWgzja; z|26ZkD_OCByukxC(FZzQ@V^uP9syrf;5v^!|F@S1OG5XuqW{?VyM6uGmmEC#X!39J zAFGd+;7%0&9n=Hn5hnl0g2Uhq!lwsk$^TUldhnzAFAN?$#PHr9%{<8d-Tn<805%V_*H~b*@IC&+JhXFpY z{>SIvLm%e$pXW#X|7Yf}$iuJpZ}o2mzWvX}L+M|8@EGp!^^5;$>XE4OKY;(seLVVM z`=9v#3wuz8SN~P}yM4$A-NOap>jUvGFpaB92y=So0y!Qott0ywz#ysvbwgm zw!X3PeQOK4y|cG}aCmrpeDd=Y_Ur8Y;_COG>zmuVJL~A!Qv?KxSBf%{T0Tp&#&)lW z2UDFe$X}~VKI!)F7dTG*DRbQWECs(wVZin|7c1Foxk!tZv})^3LK*7j&FA(#&(2CD z31a9GPwA1O(82}@jb5o^Ac0*dd~lmxPL9Oqw2kLhR(=Oq=C+NC8K2L29b{-ylKH9_ z9nwJUMhWs%Y0?TM(Kj{fo>L@JwoQ0fHMLA~m$1ZOTi~evU)h9D6I^U%wUY^`g zXQ^Di$al9gs#iEJWJ^9OvCreQ8ju()HkaI3;M_n@8uikhK=`9jP{^+>g8>ome

fjuOLaq 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 literal 0 HcmV?d00001 diff --git a/plugins/haptics/guest-js/bindings.ts b/plugins/haptics/guest-js/bindings.ts new file mode 100644 index 0000000..040e278 --- /dev/null +++ b/plugins/haptics/guest-js/bindings.ts @@ -0,0 +1,154 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +// @ts-nocheck +// This file was generated by [tauri-specta](https://github.com/oscartbeaumont/tauri-specta). Do not edit this file manually. + +/** user-defined commands **/ + +export const commands = { + async vibrate(duration: number): Promise> { + try { + return { + status: 'ok', + data: await TAURI_INVOKE('plugin:haptics|vibrate', { duration }) + } + } catch (e) { + if (e instanceof Error) throw e + else return { status: 'error', error: e as any } + } + }, + async impactFeedback( + style: ImpactFeedbackStyle + ): Promise> { + try { + return { + status: 'ok', + data: await TAURI_INVOKE('plugin:haptics|impact_feedback', { style }) + } + } catch (e) { + if (e instanceof Error) throw e + else return { status: 'error', error: e as any } + } + }, + async notificationFeedback( + type: NotificationFeedbackType + ): Promise> { + try { + return { + status: 'ok', + data: await TAURI_INVOKE('plugin:haptics|notification_feedback', { + type + }) + } + } catch (e) { + if (e instanceof Error) throw e + else return { status: 'error', error: e as any } + } + }, + async selectionFeedback(): Promise> { + try { + return { + status: 'ok', + data: await TAURI_INVOKE('plugin:haptics|selection_feedback') + } + } catch (e) { + if (e instanceof Error) throw e + else return { status: 'error', error: e as any } + } + } +} + +/** user-defined events **/ + +/* export const events = __makeEvents__<{ + randomNumber: RandomNumber; +}>({ + randomNumber: "plugin:haptics:random-number", +}); */ + +/** user-defined statics **/ + +/** user-defined types **/ + +export type Error = never +/** + * The style of an impact-feedback haptic. + * + * On iOS this maps directly to a `UIImpactFeedbackGenerator.FeedbackStyle` case. On Android, + * which has no equivalent system API, each style instead plays a distinct vibration waveform of + * increasing intensity. Has no effect on desktop platforms. Defaults to `Medium`. + */ +export type ImpactFeedbackStyle = + | 'light' + | 'medium' + | 'heavy' + | 'soft' + | 'rigid' +/** + * The type of notification feedback, indicating the outcome of a task or action. + * + * On iOS this maps directly to a `UINotificationFeedbackGenerator.FeedbackType` case. On + * Android, which has no equivalent system API, each type instead plays a distinct vibration + * waveform. Has no effect on desktop platforms. Defaults to `Success`. + */ +export type NotificationFeedbackType = 'success' | 'warning' | 'error' +//export type RandomNumber = number; + +/** tauri-specta globals **/ + +import { invoke as TAURI_INVOKE } from '@tauri-apps/api/core' +import * as TAURI_API_EVENT from '@tauri-apps/api/event' +import { type WebviewWindow as __WebviewWindow__ } from '@tauri-apps/api/webviewWindow' + +type __EventObj__ = { + listen: ( + cb: TAURI_API_EVENT.EventCallback + ) => ReturnType> + once: ( + cb: TAURI_API_EVENT.EventCallback + ) => ReturnType> + emit: T extends null + ? (payload?: T) => ReturnType + : (payload: T) => ReturnType +} + +export type Result = + | { status: 'ok'; data: T } + | { status: 'error'; error: E } + +function __makeEvents__>( + mappings: Record +) { + return new Proxy( + {} as unknown as { + [K in keyof T]: __EventObj__ & { + (handle: __WebviewWindow__): __EventObj__ + } + }, + { + get: (_, event) => { + const name = mappings[event as keyof T] + + return new Proxy((() => {}) as any, { + apply: (_, __, [window]: [__WebviewWindow__]) => ({ + listen: (arg: any) => window.listen(name, arg), + once: (arg: any) => window.once(name, arg), + emit: (arg: any) => window.emit(name, arg) + }), + get: (_, command: keyof __EventObj__) => { + switch (command) { + case 'listen': + return (arg: any) => TAURI_API_EVENT.listen(name, arg) + case 'once': + return (arg: any) => TAURI_API_EVENT.once(name, arg) + case 'emit': + return (arg: any) => TAURI_API_EVENT.emit(name, arg) + } + } + }) + } + } + ) +} diff --git a/plugins/haptics/guest-js/index.ts b/plugins/haptics/guest-js/index.ts new file mode 100644 index 0000000..9c477aa --- /dev/null +++ b/plugins/haptics/guest-js/index.ts @@ -0,0 +1,104 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +/** + * Trigger haptic feedback on Android and iOS. + * + * @module + */ + +import { commands } from './bindings' +import type { + ImpactFeedbackStyle, + NotificationFeedbackType, + Result, + Error +} from './bindings' + +/** + * Triggers a vibration for `duration` milliseconds. + * + * Has no effect on desktop platforms. + * + * @example + * ```typescript + * import { vibrate } from '@tauri-apps/plugin-haptics' + * await vibrate(300) + * ``` + * + * @param duration Duration of the vibration, in milliseconds. + * @returns A promise resolving to the {@link Result} of the operation. + * @since 2.0.0 + */ +export async function vibrate(duration: number): Promise> { + return commands.vibrate(duration) +} + +/** + * Triggers an impact-feedback haptic, indicating a collision between user interface elements. + * + * On iOS this maps to a `UIImpactFeedbackGenerator` of the given style. On Android, which has + * no equivalent system API, each style plays a distinct vibration waveform. Has no effect on + * desktop platforms. + * + * @example + * ```typescript + * import { impactFeedback } from '@tauri-apps/plugin-haptics' + * await impactFeedback('medium') + * ``` + * + * @param style The style of the impact. + * @returns A promise resolving to the {@link Result} of the operation. + * @since 2.0.0 + */ +export async function impactFeedback( + style: ImpactFeedbackStyle +): Promise> { + return commands.impactFeedback(style) +} + +/** + * Triggers a notification-feedback haptic, indicating the outcome of a task or action. + * + * On iOS this maps to a `UINotificationFeedbackGenerator` of the given type. On Android, which + * has no equivalent system API, each type plays a distinct vibration waveform. Has no effect on + * desktop platforms. + * + * @example + * ```typescript + * import { notificationFeedback } from '@tauri-apps/plugin-haptics' + * await notificationFeedback('success') + * ``` + * + * @param type The outcome to convey. + * @returns A promise resolving to the {@link Result} of the operation. + * @since 2.0.0 + */ +export async function notificationFeedback( + type: NotificationFeedbackType +): Promise> { + return commands.notificationFeedback(type) +} + +/** + * Triggers a haptic indicating that a selection changed, e.g. the value of a picker control. + * + * Has no effect on desktop platforms. + * + * @example + * ```typescript + * import { selectionFeedback } from '@tauri-apps/plugin-haptics' + * await selectionFeedback() + * ``` + * + * @returns A promise resolving to the {@link Result} of the operation. + * @since 2.0.0 + */ +export async function selectionFeedback(): Promise> { + return commands.selectionFeedback() +} + +export { ImpactFeedbackStyle, NotificationFeedbackType } from './bindings' + +// export { events }; diff --git a/plugins/haptics/ios/.gitignore b/plugins/haptics/ios/.gitignore new file mode 100644 index 0000000..5922fda --- /dev/null +++ b/plugins/haptics/ios/.gitignore @@ -0,0 +1,10 @@ +.DS_Store +/.build +/Packages +/*.xcodeproj +xcuserdata/ +DerivedData/ +.swiftpm/config/registries.json +.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata +.netrc +Package.resolved diff --git a/plugins/haptics/ios/Package.swift b/plugins/haptics/ios/Package.swift new file mode 100644 index 0000000..a64b989 --- /dev/null +++ b/plugins/haptics/ios/Package.swift @@ -0,0 +1,34 @@ +// swift-tools-version:5.3 +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +import PackageDescription + +let package = Package( + name: "tauri-plugin-haptics", + platforms: [ + .macOS(.v10_13), + .iOS(.v13), + ], + products: [ + // Products define the executables and libraries a package produces, and make them visible to other packages. + .library( + name: "tauri-plugin-haptics", + type: .static, + targets: ["tauri-plugin-haptics"]) + ], + dependencies: [ + .package(name: "Tauri", path: "../.tauri/tauri-api") + ], + targets: [ + // Targets are the basic building blocks of a package. A target can define a module or a test suite. + // Targets can depend on other targets in this package, and on products in packages this package depends on. + .target( + name: "tauri-plugin-haptics", + dependencies: [ + .byName(name: "Tauri") + ], + path: "Sources") + ] +) diff --git a/plugins/haptics/ios/README.md b/plugins/haptics/ios/README.md new file mode 100644 index 0000000..3f7138b --- /dev/null +++ b/plugins/haptics/ios/README.md @@ -0,0 +1,3 @@ +# Tauri Plugin Haptics + +A description of this package. diff --git a/plugins/haptics/ios/Sources/HapticsPlugin.swift b/plugins/haptics/ios/Sources/HapticsPlugin.swift new file mode 100644 index 0000000..4889c8a --- /dev/null +++ b/plugins/haptics/ios/Sources/HapticsPlugin.swift @@ -0,0 +1,133 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +import AudioToolbox +import CoreHaptics +import SwiftRs +import Tauri +import UIKit +import WebKit + +class ImpactFeedbackOptions: Decodable { + let style: ImpactFeedbackStyle +} + +enum ImpactFeedbackStyle: String, Decodable { + case light, medium, heavy, soft, rigid + + func into() -> UIImpactFeedbackGenerator.FeedbackStyle { + switch self { + case .light: + return .light + case .medium: + return .medium + case .heavy: + return .heavy + case .soft: + return .soft + case .rigid: + return .rigid + } + } +} + +class NotificationFeedbackOptions: Decodable { + let type: NotificationFeedbackType +} + +enum NotificationFeedbackType: String, Decodable { + case success, warning, error + + func into() -> UINotificationFeedbackGenerator.FeedbackType { + switch self { + case .success: + return .success + case .warning: + return .warning + case .error: + return .error + } + } +} + +class VibrateOptions: Decodable { + // TODO: Array + let duration: Double +} + +class HapticsPlugin: Plugin { + // + // Tauri commands + // + + @objc public func vibrate(_ invoke: Invoke) throws { + let args = try invoke.parseArgs(VibrateOptions.self) + if CHHapticEngine.capabilitiesForHardware().supportsHaptics { + do { + let engine = try CHHapticEngine() + try engine.start() + engine.resetHandler = { [] in + do { + try engine.start() + } catch { + AudioServicesPlayAlertSound(kSystemSoundID_Vibrate) + } + } + // TODO: Make some of this (or all) configurable? + let intensity: CHHapticEventParameter = CHHapticEventParameter( + parameterID: .hapticIntensity, value: 1.0) + let sharpness: CHHapticEventParameter = CHHapticEventParameter( + parameterID: .hapticSharpness, value: 1.0) + let continuousEvent = CHHapticEvent( + eventType: .hapticContinuous, + parameters: [intensity, sharpness], + relativeTime: 0.0, + duration: args.duration / 1000 + ) + let pattern = try CHHapticPattern(events: [continuousEvent], parameters: []) + let player = try engine.makePlayer(with: pattern) + + try player.start(atTime: 0) + } catch { + AudioServicesPlayAlertSound(kSystemSoundID_Vibrate) + } + } else { + AudioServicesPlayAlertSound(kSystemSoundID_Vibrate) + } + + invoke.resolve() + } + + @objc public func impactFeedback(_ invoke: Invoke) throws { + let args = try invoke.parseArgs(ImpactFeedbackOptions.self) + let generator = UIImpactFeedbackGenerator(style: args.style.into()) + generator.prepare() + generator.impactOccurred() + + invoke.resolve() + } + + @objc public func notificationFeedback(_ invoke: Invoke) throws { + let args = try invoke.parseArgs(NotificationFeedbackOptions.self) + let generator = UINotificationFeedbackGenerator() + generator.prepare() + generator.notificationOccurred(args.type.into()) + + invoke.resolve() + } + + // TODO: Consider breaking this up into Start,Change,End like capacitor + @objc public func selectionFeedback(_ invoke: Invoke) throws { + let generator = UISelectionFeedbackGenerator() + generator.prepare() + generator.selectionChanged() + + invoke.resolve() + } +} + +@_cdecl("init_plugin_haptics") +func initPlugin() -> Plugin { + return HapticsPlugin() +} diff --git a/plugins/haptics/ios/Tests/PluginTests/PluginTests.swift b/plugins/haptics/ios/Tests/PluginTests/PluginTests.swift new file mode 100644 index 0000000..99992ce --- /dev/null +++ b/plugins/haptics/ios/Tests/PluginTests/PluginTests.swift @@ -0,0 +1,12 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +import XCTest +@testable import ExamplePlugin + +final class ExamplePluginTests: XCTestCase { + func testExample() throws { + let plugin = ExamplePlugin() + } +} diff --git a/plugins/haptics/package.json b/plugins/haptics/package.json new file mode 100644 index 0000000..0a848b5 --- /dev/null +++ b/plugins/haptics/package.json @@ -0,0 +1,29 @@ +{ + "name": "@tauri-apps/plugin-haptics", + "version": "2.3.3", + "license": "MIT OR Apache-2.0", + "authors": [ + "Tauri Programme within The Commons Conservancy" + ], + "repository": "https://github.com/tauri-apps/plugins-workspace", + "type": "module", + "types": "./dist-js/index.d.ts", + "main": "./dist-js/index.cjs", + "module": "./dist-js/index.js", + "exports": { + "types": "./dist-js/index.d.ts", + "import": "./dist-js/index.js", + "require": "./dist-js/index.cjs" + }, + "scripts": { + "build": "rollup -c" + }, + "files": [ + "dist-js", + "README.md", + "LICENSE" + ], + "dependencies": { + "@tauri-apps/api": "^2.11.0" + } +} diff --git a/plugins/haptics/permissions/autogenerated/commands/impact_feedback.toml b/plugins/haptics/permissions/autogenerated/commands/impact_feedback.toml new file mode 100644 index 0000000..13fce39 --- /dev/null +++ b/plugins/haptics/permissions/autogenerated/commands/impact_feedback.toml @@ -0,0 +1,13 @@ +# Automatically generated - DO NOT EDIT! + +"$schema" = "../../schemas/schema.json" + +[[permission]] +identifier = "allow-impact-feedback" +description = "Enables the impact_feedback command without any pre-configured scope." +commands.allow = ["impact_feedback"] + +[[permission]] +identifier = "deny-impact-feedback" +description = "Denies the impact_feedback command without any pre-configured scope." +commands.deny = ["impact_feedback"] diff --git a/plugins/haptics/permissions/autogenerated/commands/notification_feedback.toml b/plugins/haptics/permissions/autogenerated/commands/notification_feedback.toml new file mode 100644 index 0000000..0c2782c --- /dev/null +++ b/plugins/haptics/permissions/autogenerated/commands/notification_feedback.toml @@ -0,0 +1,13 @@ +# Automatically generated - DO NOT EDIT! + +"$schema" = "../../schemas/schema.json" + +[[permission]] +identifier = "allow-notification-feedback" +description = "Enables the notification_feedback command without any pre-configured scope." +commands.allow = ["notification_feedback"] + +[[permission]] +identifier = "deny-notification-feedback" +description = "Denies the notification_feedback command without any pre-configured scope." +commands.deny = ["notification_feedback"] diff --git a/plugins/haptics/permissions/autogenerated/commands/selection_feedback.toml b/plugins/haptics/permissions/autogenerated/commands/selection_feedback.toml new file mode 100644 index 0000000..63e645d --- /dev/null +++ b/plugins/haptics/permissions/autogenerated/commands/selection_feedback.toml @@ -0,0 +1,13 @@ +# Automatically generated - DO NOT EDIT! + +"$schema" = "../../schemas/schema.json" + +[[permission]] +identifier = "allow-selection-feedback" +description = "Enables the selection_feedback command without any pre-configured scope." +commands.allow = ["selection_feedback"] + +[[permission]] +identifier = "deny-selection-feedback" +description = "Denies the selection_feedback command without any pre-configured scope." +commands.deny = ["selection_feedback"] diff --git a/plugins/haptics/permissions/autogenerated/commands/vibrate.toml b/plugins/haptics/permissions/autogenerated/commands/vibrate.toml new file mode 100644 index 0000000..4c2fe94 --- /dev/null +++ b/plugins/haptics/permissions/autogenerated/commands/vibrate.toml @@ -0,0 +1,13 @@ +# Automatically generated - DO NOT EDIT! + +"$schema" = "../../schemas/schema.json" + +[[permission]] +identifier = "allow-vibrate" +description = "Enables the vibrate command without any pre-configured scope." +commands.allow = ["vibrate"] + +[[permission]] +identifier = "deny-vibrate" +description = "Denies the vibrate command without any pre-configured scope." +commands.deny = ["vibrate"] diff --git a/plugins/haptics/permissions/autogenerated/reference.md b/plugins/haptics/permissions/autogenerated/reference.md new file mode 100644 index 0000000..82a911b --- /dev/null +++ b/plugins/haptics/permissions/autogenerated/reference.md @@ -0,0 +1,113 @@ +## Permission Table + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
IdentifierDescription
+ +`haptics:allow-impact-feedback` + + + +Enables the impact_feedback command without any pre-configured scope. + +
+ +`haptics:deny-impact-feedback` + + + +Denies the impact_feedback command without any pre-configured scope. + +
+ +`haptics:allow-notification-feedback` + + + +Enables the notification_feedback command without any pre-configured scope. + +
+ +`haptics:deny-notification-feedback` + + + +Denies the notification_feedback command without any pre-configured scope. + +
+ +`haptics:allow-selection-feedback` + + + +Enables the selection_feedback command without any pre-configured scope. + +
+ +`haptics:deny-selection-feedback` + + + +Denies the selection_feedback command without any pre-configured scope. + +
+ +`haptics:allow-vibrate` + + + +Enables the vibrate command without any pre-configured scope. + +
+ +`haptics:deny-vibrate` + + + +Denies the vibrate command without any pre-configured scope. + +
diff --git a/plugins/haptics/permissions/schemas/schema.json b/plugins/haptics/permissions/schemas/schema.json new file mode 100644 index 0000000..ed217dc --- /dev/null +++ b/plugins/haptics/permissions/schemas/schema.json @@ -0,0 +1,348 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "PermissionFile", + "description": "Permission file that can define a default permission, a set of permissions or a list of inlined permissions.", + "type": "object", + "properties": { + "default": { + "description": "The default permission set for the plugin", + "anyOf": [ + { + "$ref": "#/definitions/DefaultPermission" + }, + { + "type": "null" + } + ] + }, + "set": { + "description": "A list of permissions sets defined", + "type": "array", + "items": { + "$ref": "#/definitions/PermissionSet" + } + }, + "permission": { + "description": "A list of inlined permissions", + "default": [], + "type": "array", + "items": { + "$ref": "#/definitions/Permission" + } + } + }, + "definitions": { + "DefaultPermission": { + "description": "The default permission set of the plugin.\n\nWorks similarly to a permission with the \"default\" identifier.", + "type": "object", + "required": [ + "permissions" + ], + "properties": { + "version": { + "description": "The version of the permission.", + "type": [ + "integer", + "null" + ], + "format": "uint64", + "minimum": 1.0 + }, + "description": { + "description": "Human-readable description of what the permission does. Tauri convention is to use `

` headings in markdown content for Tauri documentation generation purposes.", + "type": [ + "string", + "null" + ] + }, + "permissions": { + "description": "All permissions this set contains.", + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "PermissionSet": { + "description": "A set of direct permissions grouped together under a new name.", + "type": "object", + "required": [ + "description", + "identifier", + "permissions" + ], + "properties": { + "identifier": { + "description": "A unique identifier for the permission.", + "type": "string" + }, + "description": { + "description": "Human-readable description of what the permission does.", + "type": "string" + }, + "permissions": { + "description": "All permissions this set contains.", + "type": "array", + "items": { + "$ref": "#/definitions/PermissionKind" + } + } + } + }, + "Permission": { + "description": "Descriptions of explicit privileges of commands.\n\nIt can enable commands to be accessible in the frontend of the application.\n\nIf the scope is defined it can be used to fine grain control the access of individual or multiple commands.", + "type": "object", + "required": [ + "identifier" + ], + "properties": { + "version": { + "description": "The version of the permission.", + "type": [ + "integer", + "null" + ], + "format": "uint64", + "minimum": 1.0 + }, + "identifier": { + "description": "A unique identifier for the permission.", + "type": "string" + }, + "description": { + "description": "Human-readable description of what the permission does. Tauri internal convention is to use `

` headings in markdown content for Tauri documentation generation purposes.", + "type": [ + "string", + "null" + ] + }, + "commands": { + "description": "Allowed or denied commands when using this permission.", + "default": { + "allow": [], + "deny": [] + }, + "allOf": [ + { + "$ref": "#/definitions/Commands" + } + ] + }, + "scope": { + "description": "Allowed or denied scoped when using this permission.", + "allOf": [ + { + "$ref": "#/definitions/Scopes" + } + ] + }, + "platforms": { + "description": "Target platforms this permission applies. By default all platforms are affected by this permission.", + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/Target" + } + } + } + }, + "Commands": { + "description": "Allowed and denied commands inside a permission.\n\nIf two commands clash inside of `allow` and `deny`, it should be denied by default.", + "type": "object", + "properties": { + "allow": { + "description": "Allowed command.", + "default": [], + "type": "array", + "items": { + "type": "string" + } + }, + "deny": { + "description": "Denied command, which takes priority.", + "default": [], + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "Scopes": { + "description": "An argument for fine grained behavior control of Tauri commands.\n\nIt can be of any serde serializable type and is used to allow or prevent certain actions inside a Tauri command. The configured scope is passed to the command and will be enforced by the command implementation.\n\n## Example\n\n```json { \"allow\": [{ \"path\": \"$HOME/**\" }], \"deny\": [{ \"path\": \"$HOME/secret.txt\" }] } ```", + "type": "object", + "properties": { + "allow": { + "description": "Data that defines what is allowed by the scope.", + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/Value" + } + }, + "deny": { + "description": "Data that defines what is denied by the scope. This should be prioritized by validation logic.", + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/Value" + } + } + } + }, + "Value": { + "description": "All supported ACL values.", + "anyOf": [ + { + "description": "Represents a null JSON value.", + "type": "null" + }, + { + "description": "Represents a [`bool`].", + "type": "boolean" + }, + { + "description": "Represents a valid ACL [`Number`].", + "allOf": [ + { + "$ref": "#/definitions/Number" + } + ] + }, + { + "description": "Represents a [`String`].", + "type": "string" + }, + { + "description": "Represents a list of other [`Value`]s.", + "type": "array", + "items": { + "$ref": "#/definitions/Value" + } + }, + { + "description": "Represents a map of [`String`] keys to [`Value`]s.", + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/Value" + } + } + ] + }, + "Number": { + "description": "A valid ACL number.", + "anyOf": [ + { + "description": "Represents an [`i64`].", + "type": "integer", + "format": "int64" + }, + { + "description": "Represents a [`f64`].", + "type": "number", + "format": "double" + } + ] + }, + "Target": { + "description": "Platform target.", + "oneOf": [ + { + "description": "MacOS.", + "type": "string", + "enum": [ + "macOS" + ] + }, + { + "description": "Windows.", + "type": "string", + "enum": [ + "windows" + ] + }, + { + "description": "Linux.", + "type": "string", + "enum": [ + "linux" + ] + }, + { + "description": "Android.", + "type": "string", + "enum": [ + "android" + ] + }, + { + "description": "iOS.", + "type": "string", + "enum": [ + "iOS" + ] + } + ] + }, + "PermissionKind": { + "type": "string", + "oneOf": [ + { + "description": "Enables the impact_feedback command without any pre-configured scope.", + "type": "string", + "const": "allow-impact-feedback", + "markdownDescription": "Enables the impact_feedback command without any pre-configured scope." + }, + { + "description": "Denies the impact_feedback command without any pre-configured scope.", + "type": "string", + "const": "deny-impact-feedback", + "markdownDescription": "Denies the impact_feedback command without any pre-configured scope." + }, + { + "description": "Enables the notification_feedback command without any pre-configured scope.", + "type": "string", + "const": "allow-notification-feedback", + "markdownDescription": "Enables the notification_feedback command without any pre-configured scope." + }, + { + "description": "Denies the notification_feedback command without any pre-configured scope.", + "type": "string", + "const": "deny-notification-feedback", + "markdownDescription": "Denies the notification_feedback command without any pre-configured scope." + }, + { + "description": "Enables the selection_feedback command without any pre-configured scope.", + "type": "string", + "const": "allow-selection-feedback", + "markdownDescription": "Enables the selection_feedback command without any pre-configured scope." + }, + { + "description": "Denies the selection_feedback command without any pre-configured scope.", + "type": "string", + "const": "deny-selection-feedback", + "markdownDescription": "Denies the selection_feedback command without any pre-configured scope." + }, + { + "description": "Enables the vibrate command without any pre-configured scope.", + "type": "string", + "const": "allow-vibrate", + "markdownDescription": "Enables the vibrate command without any pre-configured scope." + }, + { + "description": "Denies the vibrate command without any pre-configured scope.", + "type": "string", + "const": "deny-vibrate", + "markdownDescription": "Denies the vibrate command without any pre-configured scope." + } + ] + } + } +} \ No newline at end of file diff --git a/plugins/haptics/rollup.config.js b/plugins/haptics/rollup.config.js new file mode 100644 index 0000000..1f349ec --- /dev/null +++ b/plugins/haptics/rollup.config.js @@ -0,0 +1,7 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +import { createConfig } from '../../shared/rollup.config.js' + +export default createConfig() diff --git a/plugins/haptics/src/commands.rs b/plugins/haptics/src/commands.rs new file mode 100644 index 0000000..76f097c --- /dev/null +++ b/plugins/haptics/src/commands.rs @@ -0,0 +1,33 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use tauri::{command, AppHandle, Runtime}; + +use crate::{HapticsExt, ImpactFeedbackStyle, NotificationFeedbackType, Result}; + +#[command] +pub(crate) async fn vibrate(app: AppHandle, duration: u32) -> Result<()> { + app.haptics().vibrate(duration) +} + +#[command] +pub(crate) async fn impact_feedback( + app: AppHandle, + style: ImpactFeedbackStyle, +) -> Result<()> { + app.haptics().impact_feedback(style) +} + +#[command] +pub(crate) async fn notification_feedback( + app: AppHandle, + r#type: NotificationFeedbackType, +) -> Result<()> { + app.haptics().notification_feedback(r#type) +} + +#[command] +pub(crate) async fn selection_feedback(app: AppHandle) -> Result<()> { + app.haptics().selection_feedback() +} diff --git a/plugins/haptics/src/desktop.rs b/plugins/haptics/src/desktop.rs new file mode 100644 index 0000000..d036edd --- /dev/null +++ b/plugins/haptics/src/desktop.rs @@ -0,0 +1,44 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use serde::de::DeserializeOwned; +use tauri::{plugin::PluginApi, AppHandle, Runtime}; + +use crate::models::*; + +pub fn init( + app: &AppHandle, + _api: PluginApi, +) -> crate::Result> { + Ok(Haptics(app.clone())) +} + +/// Access to the haptics APIs. +pub struct Haptics(AppHandle); + +impl Haptics { + /// No-op on desktop; haptic feedback is not supported on Linux, macOS or Windows. Always + /// returns `Ok`. + pub fn vibrate(&self, _duration: u32) -> crate::Result<()> { + Ok(()) + } + + /// No-op on desktop; haptic feedback is not supported on Linux, macOS or Windows. Always + /// returns `Ok`. + pub fn impact_feedback(&self, _style: ImpactFeedbackStyle) -> crate::Result<()> { + Ok(()) + } + + /// No-op on desktop; haptic feedback is not supported on Linux, macOS or Windows. Always + /// returns `Ok`. + pub fn notification_feedback(&self, _type: NotificationFeedbackType) -> crate::Result<()> { + Ok(()) + } + + /// No-op on desktop; haptic feedback is not supported on Linux, macOS or Windows. Always + /// returns `Ok`. + pub fn selection_feedback(&self) -> crate::Result<()> { + Ok(()) + } +} diff --git a/plugins/haptics/src/error.rs b/plugins/haptics/src/error.rs new file mode 100644 index 0000000..3a24755 --- /dev/null +++ b/plugins/haptics/src/error.rs @@ -0,0 +1,35 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use serde::{ser::Serializer, Serialize}; + +/// Alias for a [`std::result::Result`] with the error type [`Error`]. +pub type Result = std::result::Result; + +// TODO: Improve Error handling (different typed errors instead of one (stringified) PluginInvokeError for all mobile errors) + +/// Errors returned by the haptics APIs. +#[derive(Debug, thiserror::Error)] +#[cfg_attr(feature = "specta", derive(specta::Type))] +pub enum Error { + /// The call to the Android or iOS plugin implementation failed, either because the + /// arguments could not be serialized, the response could not be deserialized, or the + /// native side rejected the call. Only produced on Android and iOS. + #[cfg(mobile)] + #[error(transparent)] + PluginInvoke( + #[cfg_attr(feature = "specta", specta(skip))] + #[from] + tauri::plugin::mobile::PluginInvokeError, + ), +} + +impl Serialize for Error { + fn serialize(&self, serializer: S) -> std::result::Result + where + S: Serializer, + { + serializer.serialize_str(self.to_string().as_ref()) + } +} diff --git a/plugins/haptics/src/lib.rs b/plugins/haptics/src/lib.rs new file mode 100644 index 0000000..7984beb --- /dev/null +++ b/plugins/haptics/src/lib.rs @@ -0,0 +1,64 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +//! Trigger haptic feedback on supported devices. +//! +//! ## Cargo features +//! +//! - **specta**: Add support for [`specta::specta`](https://docs.rs/specta/2.0.0-rc.25/specta/attr.specta.html) on structs like [`ImpactFeedbackStyle`], [`NotificationFeedbackType`]. + +use tauri::{ + plugin::{Builder, TauriPlugin}, + Manager, Runtime, +}; + +pub use models::*; + +#[cfg(desktop)] +mod desktop; +#[cfg(mobile)] +mod mobile; + +mod commands; +mod error; +mod models; + +pub use error::{Error, Result}; + +#[cfg(desktop)] +pub use desktop::Haptics; +#[cfg(mobile)] +pub use mobile::Haptics; + +/// Extensions to [`tauri::App`], [`tauri::AppHandle`], [`tauri::WebviewWindow`], [`tauri::Webview`] and [`tauri::Window`] to access the haptics APIs. +pub trait HapticsExt { + /// Returns a handle to the [`Haptics`] APIs. + fn haptics(&self) -> &Haptics; +} + +impl> crate::HapticsExt for T { + fn haptics(&self) -> &Haptics { + self.state::>().inner() + } +} + +/// Initializes the plugin. +pub fn init() -> TauriPlugin { + Builder::new("haptics") + .invoke_handler(tauri::generate_handler![ + commands::vibrate, + commands::impact_feedback, + commands::notification_feedback, + commands::selection_feedback + ]) + .setup(|app, api| { + #[cfg(mobile)] + let haptics = mobile::init(app, api)?; + #[cfg(desktop)] + let haptics = desktop::init(app, api)?; + app.manage(haptics); + Ok(()) + }) + .build() +} diff --git a/plugins/haptics/src/mobile.rs b/plugins/haptics/src/mobile.rs new file mode 100644 index 0000000..42ae245 --- /dev/null +++ b/plugins/haptics/src/mobile.rs @@ -0,0 +1,105 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use serde::{de::DeserializeOwned, Serialize}; +use tauri::{ + plugin::{PluginApi, PluginHandle}, + AppHandle, Runtime, +}; + +use crate::models::*; + +#[cfg(target_os = "android")] +const PLUGIN_IDENTIFIER: &str = "app.tauri.haptics"; + +#[cfg(target_os = "ios")] +tauri::ios_plugin_binding!(init_plugin_haptics); + +// initializes the Kotlin or Swift plugin classes +pub fn init( + _app: &AppHandle, + api: PluginApi, +) -> crate::Result> { + #[cfg(target_os = "android")] + let handle = api.register_android_plugin(PLUGIN_IDENTIFIER, "HapticsPlugin")?; + #[cfg(target_os = "ios")] + let handle = api.register_ios_plugin(init_plugin_haptics)?; + Ok(Haptics(handle)) +} + +/// Access to the haptics APIs. +pub struct Haptics(PluginHandle); + +impl Haptics { + /// Triggers a vibration for `duration` milliseconds. + /// + /// On iOS this plays a continuous [Core Haptics](https://developer.apple.com/documentation/corehaptics) + /// pattern when the device supports it, falling back to the system alert vibration + /// otherwise. On Android it uses [`Vibrator`](https://developer.android.com/reference/android/os/Vibrator). + /// + /// # Errors + /// + /// Returns [`Error::PluginInvoke`](crate::Error::PluginInvoke) if the underlying Android or + /// iOS plugin invocation fails. + pub fn vibrate(&self, duration: u32) -> crate::Result<()> { + self.0 + .run_mobile_plugin("vibrate", VibratePayload { duration }) + .map_err(Into::into) + } + + /// Triggers an impact-feedback haptic with the given [`ImpactFeedbackStyle`]. + /// + /// # Errors + /// + /// Returns [`Error::PluginInvoke`](crate::Error::PluginInvoke) if the underlying Android or + /// iOS plugin invocation fails. + pub fn impact_feedback(&self, style: ImpactFeedbackStyle) -> crate::Result<()> { + self.0 + .run_mobile_plugin("impactFeedback", ImpactFeedbackPayload { style }) + .map_err(Into::into) + } + + /// Triggers a notification-feedback haptic for the given [`NotificationFeedbackType`]. + /// + /// # Errors + /// + /// Returns [`Error::PluginInvoke`](crate::Error::PluginInvoke) if the underlying Android or + /// iOS plugin invocation fails. + pub fn notification_feedback(&self, r#type: NotificationFeedbackType) -> crate::Result<()> { + self.0 + .run_mobile_plugin( + "notificationFeedback", + NotificationFeedbackPayload { r#type }, + ) + .map_err(Into::into) + } + + /// Triggers a haptic indicating that a selection changed, e.g. when the value of a picker + /// control changes. + /// + /// # Errors + /// + /// Returns [`Error::PluginInvoke`](crate::Error::PluginInvoke) if the underlying Android or + /// iOS plugin invocation fails. + pub fn selection_feedback(&self) -> crate::Result<()> { + self.0 + .run_mobile_plugin("selectionFeedback", ()) + .map_err(Into::into) + } +} + +#[derive(Serialize)] +struct VibratePayload { + duration: u32, +} + +#[derive(Serialize)] +struct ImpactFeedbackPayload { + style: ImpactFeedbackStyle, +} + +#[derive(Serialize)] +struct NotificationFeedbackPayload { + r#type: NotificationFeedbackType, +} diff --git a/plugins/haptics/src/models.rs b/plugins/haptics/src/models.rs new file mode 100644 index 0000000..d59cc20 --- /dev/null +++ b/plugins/haptics/src/models.rs @@ -0,0 +1,54 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use serde::{Deserialize, Serialize}; +/* +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[cfg_attr(feature = "specta", derive(specta::Type))] +#[serde(rename_all = "camelCase")] +pub struct HapticsOptions { + // TODO: support array to match web api + pub duration: u32, +} + */ + +/// The style of an impact-feedback haptic. +/// +/// On iOS this maps directly to a `UIImpactFeedbackGenerator.FeedbackStyle` case. On Android, +/// which has no equivalent system API, each style instead plays a distinct vibration waveform of +/// increasing intensity. Has no effect on desktop platforms. Defaults to `Medium`. +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +#[cfg_attr(feature = "specta", derive(specta::Type))] +#[serde(rename_all = "camelCase")] +pub enum ImpactFeedbackStyle { + /// A collision between small, light user interface elements. + Light, + /// A collision between moderately sized user interface elements. + #[default] + Medium, + /// A collision between large, heavy user interface elements. + Heavy, + /// A soft, muted impact between user interface elements. + Soft, + /// A sharp, rigid impact between user interface elements. + Rigid, +} + +/// The type of notification feedback, indicating the outcome of a task or action. +/// +/// On iOS this maps directly to a `UINotificationFeedbackGenerator.FeedbackType` case. On +/// Android, which has no equivalent system API, each type instead plays a distinct vibration +/// waveform. Has no effect on desktop platforms. Defaults to `Success`. +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +#[cfg_attr(feature = "specta", derive(specta::Type))] +#[serde(rename_all = "camelCase")] +pub enum NotificationFeedbackType { + /// A task or action has completed successfully. + #[default] + Success, + /// A task or action has produced a warning. + Warning, + /// A task or action has failed. + Error, +} diff --git a/plugins/haptics/tsconfig.json b/plugins/haptics/tsconfig.json new file mode 100644 index 0000000..4374468 --- /dev/null +++ b/plugins/haptics/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "rootDir": "guest-js" + }, + "include": ["guest-js/*.ts"] +} diff --git a/plugins/http/CHANGELOG.md b/plugins/http/CHANGELOG.md new file mode 100644 index 0000000..ed6b948 --- /dev/null +++ b/plugins/http/CHANGELOG.md @@ -0,0 +1,352 @@ +# Changelog + +## [2.7.0] + +- [`1198a524`](https://github.com/tauri-apps/plugins-workspace/commit/1198a524b710abf2abeb1d9bd7b252402d26ca6d) **Security:** Added the `scopeRedirects` plugin configuration option, which checks the URL scope on every hop of a redirect chain instead of only on the URL requested by the frontend. Without it, a server on an allowed origin can redirect the request to any other origin - including `localhost` services, internal hosts and cloud metadata endpoints - and the plugin follows it, returning the response to the webview. + + ```json + { + "plugins": { + "http": { + "scopeRedirects": true + } + } + } + ``` + + It is opt-in because a redirect to a URL that is not allowed by the scope now fails with `url not allowed on the configured scope` instead of being followed, so applications that rely on being redirected outside of their scope must add the redirect target to the scope. **This will become the default in v3.** + + Note that `tauri_plugin_http::init()` now returns `TauriPlugin>` instead of `TauriPlugin`. + +## [2.6.1] + +- [`a21555dd`](https://github.com/tauri-apps/plugins-workspace/commit/a21555ddd2eaadfed23848912fe2802c2ba7579e) ([#3566](https://github.com/tauri-apps/plugins-workspace/pull/3566) by [@followdarko](https://github.com/tauri-apps/plugins-workspace/../../followdarko)) Fix unhandled promise rejections on every `fetch` teardown: the request/body cleanup commands were fired as floating promises, and releasing an already-released resource rejects with `The resource id N is invalid.`. `dropBody` is now idempotent and both cleanup calls handle their own rejection. + +## [2.6.0] + +- [`f8053e65`](https://github.com/tauri-apps/plugins-workspace/commit/f8053e659e4ccd85c1f52833411ff8417cbc5e69) ([#3527](https://github.com/tauri-apps/plugins-workspace/pull/3527) by [@Legend-Master](https://github.com/tauri-apps/plugins-workspace/../../Legend-Master)) Documented Cargo feature flags in each plugin's crate-level documentation. +- [`19fb3926`](https://github.com/tauri-apps/plugins-workspace/commit/19fb3926fd24d95371bc3a7593256861df406436) ([#3528](https://github.com/tauri-apps/plugins-workspace/pull/3528) by [@Legend-Master](https://github.com/tauri-apps/plugins-workspace/../../Legend-Master)) Added `system-proxy` feature to replace the old `macos-system-configuration` feature flag to match the new reqwest API. + +### Dependencies + +- Upgraded to `fs-js@2.5.2` + +## \[2.5.9] + +### Dependencies + +- Upgraded to `fs-js@2.5.1` + +## \[2.5.8] + +- [`29712892`](https://github.com/tauri-apps/plugins-workspace/commit/29712892526cfc2d35c9002e0a56925084ae1b73) ([#3252](https://github.com/tauri-apps/plugins-workspace/pull/3252) by [@NVolcz](https://github.com/tauri-apps/plugins-workspace/../../NVolcz)) Correct Response header initialization to support cloning and ensure Set-Cookie visibility. + +### Dependencies + +- Upgraded to `fs-js@2.5.0` + +## \[2.5.7] + +- [`61e9b0ab`](https://github.com/tauri-apps/plugins-workspace/commit/61e9b0ab64c56cc5f7a5cb3b92b386671da6e0a2) ([#3228](https://github.com/tauri-apps/plugins-workspace/pull/3228)) Cleanup resource when the returned `ReadableStream.cancel` is called to avoid memory leaks + +## \[2.5.6] + +- [`b1dbee2c`](https://github.com/tauri-apps/plugins-workspace/commit/b1dbee2c55c1aa7b64732c4cc9f8cb20520b8666) ([#3210](https://github.com/tauri-apps/plugins-workspace/pull/3210) by [@FabianLars](https://github.com/tauri-apps/plugins-workspace/../../FabianLars)) Fixed an issue that caused the Origin header to always be `null` on macOS, iOS and Linux. + +## \[2.5.5] + +- [`e8915f17`](https://github.com/tauri-apps/plugins-workspace/commit/e8915f17e418138f0776870353cd6ce7254b0473) ([#2562](https://github.com/tauri-apps/plugins-workspace/pull/2562) by [@amrbashir](https://github.com/tauri-apps/plugins-workspace/../../amrbashir)) Fix aborting a request in the middle of a streaming response. + +### Dependencies + +- Upgraded to `fs-js@2.4.5` + +## \[2.5.4] + +- [`93426f85`](https://github.com/tauri-apps/plugins-workspace/commit/93426f85120f49beb9f40222bff45185a32d54a9) Fixed an issue that caused docs.rs builds to fail. No user facing changes. + +### Dependencies + +- Upgraded to `fs-js@2.4.4` + +## \[2.5.3] + +### Dependencies + +- Upgraded to `fs-js@2.4.3` + +## \[2.5.2] + +### Dependencies + +- Upgraded to `fs-js@2.4.2` + +## \[2.5.1] + +### Dependencies + +- Upgraded to `fs-js@2.4.1` + +## \[2.5.0] + +- [`f209b2f2`](https://github.com/tauri-apps/plugins-workspace/commit/f209b2f23cb29133c97ad5961fb46ef794dbe063) ([#2804](https://github.com/tauri-apps/plugins-workspace/pull/2804) by [@renovate](https://github.com/tauri-apps/plugins-workspace/../../renovate)) Updated tauri to 2.6 + +### Dependencies + +- Upgraded to `fs-js@2.4.0` + +## \[2.4.4] + +- [`ff384cba`](https://github.com/tauri-apps/plugins-workspace/commit/ff384cbabe82ae715798a4ee49fd07ffcfbcdb5d) ([#2636](https://github.com/tauri-apps/plugins-workspace/pull/2636) by [@asomethings](https://github.com/tauri-apps/plugins-workspace/../../asomethings)) Properly handle responses with status code 204. + +### Dependencies + +- Upgraded to `fs-js@2.3.0` + +## \[2.4.3] + +- [`37c0477a`](https://github.com/tauri-apps/plugins-workspace/commit/37c0477afe926d326573f1827045875ce8bf8187) ([#2561](https://github.com/tauri-apps/plugins-workspace/pull/2561)) Add `zstd` cargo feature flag to enable `reqwest/zstd` flag. +- [`9ebbfb2e`](https://github.com/tauri-apps/plugins-workspace/commit/9ebbfb2e3ccef8e0f277a0c02fe6b399b41feeb6) ([#1978](https://github.com/tauri-apps/plugins-workspace/pull/1978)) Persist cookies to disk and load it on next app start. + +### Dependencies + +- Upgraded to `fs-js@2.2.1` + +## \[2.4.2] + +- [`a15eedf3`](https://github.com/tauri-apps/plugins-workspace/commit/a15eedf37854344f7ffbcb0d373d848563817011) ([#2535](https://github.com/tauri-apps/plugins-workspace/pull/2535) by [@amrbashir](https://github.com/tauri-apps/plugins-workspace/../../amrbashir)) Fix `fetch` occasionally throwing an error due to trying to close the underline stream twice. + +## \[2.4.1] + +- [`d3183aa9`](https://github.com/tauri-apps/plugins-workspace/commit/d3183aa99da7ca67e627394132ddeb3b85ccef06) ([#2522](https://github.com/tauri-apps/plugins-workspace/pull/2522) by [@adrieljss](https://github.com/tauri-apps/plugins-workspace/../../adrieljss)) Fix `fetch` blocking until the whole response is read even if it was a streaming response. + +## \[2.4.0] + +- [`cb38f54f`](https://github.com/tauri-apps/plugins-workspace/commit/cb38f54f4a4ef30995283cd82166c62da17bac44) ([#2479](https://github.com/tauri-apps/plugins-workspace/pull/2479) by [@adrieljss](https://github.com/tauri-apps/plugins-workspace/../../adrieljss)) Add stream support for HTTP stream responses. + +## \[2.3.0] + +- [`10513649`](https://github.com/tauri-apps/plugins-workspace/commit/105136494c5a5bf4b1f1cc06cc71815412d17ec8) ([#2204](https://github.com/tauri-apps/plugins-workspace/pull/2204) by [@RickeyWard](https://github.com/tauri-apps/plugins-workspace/../../RickeyWard)) Add `dangerous-settings` feature flag and new JS `danger` option to disable tls hostname/certificate validation. + +## \[2.2.0] + +- [`3a79266b`](https://github.com/tauri-apps/plugins-workspace/commit/3a79266b8cf96a55b1ae6339d725567d45a44b1d) ([#2173](https://github.com/tauri-apps/plugins-workspace/pull/2173) by [@FabianLars](https://github.com/tauri-apps/plugins-workspace/../../FabianLars)) Bumped all plugins to `v2.2.0`. From now, the versions for the Rust and JavaScript packages of each plugin will be in sync with each other. + +### Dependencies + +- Upgraded to `fs@2.2.0` + +## \[2.0.2] + +### Dependencies + +- Upgraded to `fs-js@2.0.4` + +## \[2.0.4] + +- [`a3b553dd`](https://github.com/tauri-apps/plugins-workspace/commit/a3b553ddb403771aa699362c4e69a064b7731da5) ([#2079](https://github.com/tauri-apps/plugins-workspace/pull/2079) by [@amrbashir](https://github.com/tauri-apps/plugins-workspace/../../amrbashir)) Add tracing logs for requestes and responses behind `tracing` feature flag. + +### Dependencies + +- Upgraded to `fs@2.1.0` + +## \[2.0.3] + +### Dependencies + +- Upgraded to `fs@2.0.3` + +## \[2.0.1] + +- [`cfd48b3b`](https://github.com/tauri-apps/plugins-workspace/commit/cfd48b3b2ec0fccfc162197518694ed59ceda22c) ([#1941](https://github.com/tauri-apps/plugins-workspace/pull/1941) by [@Nipsuli](https://github.com/tauri-apps/plugins-workspace/../../Nipsuli)) Allow skipping sending `Origin` header in HTTP requests by setting `Origin` header to an empty string when calling `fetch`. +- [`9b2840db`](https://github.com/tauri-apps/plugins-workspace/commit/9b2840db9464cf08db75806270e4441f0af81e5d) ([#1884](https://github.com/tauri-apps/plugins-workspace/pull/1884) by [@amrbashir](https://github.com/tauri-apps/plugins-workspace/../../amrbashir)) Retain headers order. + +## \[2.0.1] + +- [`a1a82208`](https://github.com/tauri-apps/plugins-workspace/commit/a1a82208ed4ab87f83310be0dc95428aec9ab241) ([#1873](https://github.com/tauri-apps/plugins-workspace/pull/1873) by [@lucasfernog](https://github.com/tauri-apps/plugins-workspace/../../lucasfernog)) Downgrade MSRV to 1.77.2 to support Windows 7. + +### Dependencies + +- Upgraded to `fs@2.0.1` + +## \[2.0.0] + +- [`e2c4dfb6`](https://github.com/tauri-apps/plugins-workspace/commit/e2c4dfb6af43e5dd8d9ceba232c315f5febd55c1) Update to tauri v2 stable release. + +### Dependencies + +- Upgraded to `fs@2.0.0` + +## \[2.0.0-rc.6] + +### Dependencies + +- Upgraded to `fs@2.0.0-rc.6` + +## \[2.0.0-rc.5] + +### Dependencies + +- Upgraded to `fs@2.0.0-rc.5` + +## \[2.0.0-rc.4] + +### Dependencies + +- Upgraded to `fs@2.0.0-rc.4` + +## \[2.0.0-rc.3] + +### Dependencies + +- Upgraded to `fs@2.0.0-rc.3` + +## \[2.0.0-rc.2] + +### Dependencies + +- Upgraded to `fs@2.0.0-rc.2` + +## \[2.0.0-rc.2] + +- [`e2e97db5`](https://github.com/tauri-apps/plugins-workspace/commit/e2e97db51983267f5be84d4f6f0278d58834d1f5) ([#1701](https://github.com/tauri-apps/plugins-workspace/pull/1701) by [@lucasfernog](https://github.com/tauri-apps/plugins-workspace/../../lucasfernog)) Update to tauri 2.0.0-rc.8 + +## \[2.0.0-rc.1] + +- [`84f8bd5e`](https://github.com/tauri-apps/plugins-workspace/commit/84f8bd5e1ef22e664267380b5bbf756ebc5389c3) ([#1662](https://github.com/tauri-apps/plugins-workspace/pull/1662) by [@twlite](https://github.com/tauri-apps/plugins-workspace/../../twlite)) Fixed an issue with abort signal not aborting the fetch request. + +## \[2.0.0-rc.0] + +- [`9887d1`](https://github.com/tauri-apps/plugins-workspace/commit/9887d14bd0e971c4c0f5c1188fc4005d3fc2e29e) Update to tauri RC. + +### Dependencies + +- Upgraded to `fs@2.0.0-rc.0` + +## \[2.0.0-beta.9] + +- [`99d6ac0f`](https://github.com/tauri-apps/plugins-workspace/commit/99d6ac0f9506a6a4a1aa59c728157190a7441af6) ([#1606](https://github.com/tauri-apps/plugins-workspace/pull/1606) by [@FabianLars](https://github.com/tauri-apps/plugins-workspace/../../FabianLars)) The JS packages now specify the *minimum* `@tauri-apps/api` version instead of a single exact version. +- [`6de87966`](https://github.com/tauri-apps/plugins-workspace/commit/6de87966ecc00ad9d91c25be452f1f46bd2b7e1f) ([#1597](https://github.com/tauri-apps/plugins-workspace/pull/1597) by [@Legend-Master](https://github.com/tauri-apps/plugins-workspace/../../Legend-Master)) Update to tauri beta.25. + +## \[2.0.0-beta.8] + +- [`ac9a25cc`](https://github.com/tauri-apps/plugins-workspace/commit/ac9a25cc12ee2b325f00212ba74316da3369bde5) ([#1395](https://github.com/tauri-apps/plugins-workspace/pull/1395) by [@amrbashir](https://github.com/tauri-apps/plugins-workspace/../../amrbashir)) Fix cancelling requests using `AbortSignal`. +- [`a6654932`](https://github.com/tauri-apps/plugins-workspace/commit/a66549329c60dea35e3a06a38c357e368c9053a1) ([#1526](https://github.com/tauri-apps/plugins-workspace/pull/1526) by [@amrbashir](https://github.com/tauri-apps/plugins-workspace/../../amrbashir)) Fix missing `Set-Cookie` headers in the response which meant `request.headers.getSetCookie()` always returned empty array. +- [`22a17980`](https://github.com/tauri-apps/plugins-workspace/commit/22a17980ff4f6f8c40adb1b8f4ffc6dae2fe7e30) ([#1537](https://github.com/tauri-apps/plugins-workspace/pull/1537) by [@lucasfernog](https://github.com/tauri-apps/plugins-workspace/../../lucasfernog)) Update to tauri beta.24. + +## \[2.0.0-beta.7] + +- [`76daee7a`](https://github.com/tauri-apps/plugins-workspace/commit/76daee7aafece34de3092c86e531cf9eb1138989) ([#1512](https://github.com/tauri-apps/plugins-workspace/pull/1512) by [@renovate](https://github.com/tauri-apps/plugins-workspace/../../renovate)) Update to tauri beta.23. + +## \[2.0.0-beta.6] + +- [`0f739dbc`](https://github.com/tauri-apps/plugins-workspace/commit/0f739dbc483a1f091977cbe575c3862fd39f8cf1) ([#1392](https://github.com/tauri-apps/plugins-workspace/pull/1392) by [@amrbashir](https://github.com/tauri-apps/plugins-workspace/../../amrbashir)) Allow setting `Origin` header when `unsafe-headers` feature flag is active. + +## \[2.0.0-beta.5] + +- [`9013854f`](https://github.com/tauri-apps/plugins-workspace/commit/9013854f42a49a230b9dbb9d02774765528a923f)([#1382](https://github.com/tauri-apps/plugins-workspace/pull/1382)) Update to tauri beta.22. + +## \[2.0.0-beta.4] + +- [`9d7ae45b`](https://github.com/tauri-apps/plugins-workspace/commit/9d7ae45b0edf9b22c73e7d7c413a784bb35c3d77)([#1354](https://github.com/tauri-apps/plugins-workspace/pull/1354)) Include headers created by browser if not declared by user, which fixes missing headers like `Content-Type` when using `FormData`. +- [`430bd6f4`](https://github.com/tauri-apps/plugins-workspace/commit/430bd6f4f379bee5d232ae6b098ae131db7f178a)([#1363](https://github.com/tauri-apps/plugins-workspace/pull/1363)) Update to tauri beta.20. + +## \[2.0.0-beta.3] + +- [`bd1ed590`](https://github.com/tauri-apps/plugins-workspace/commit/bd1ed5903ffcce5500310dac1e59e8c67674ef1e)([#1237](https://github.com/tauri-apps/plugins-workspace/pull/1237)) Update to tauri beta.17. + +## \[2.0.0-beta.6] + +### Dependencies + +- Upgraded to `fs@2.0.0-beta.6` + +## \[2.0.0-beta.5] + +- [`500ff10`](https://github.com/tauri-apps/plugins-workspace/commit/500ff10fbd89fdfc73caf9d153029dad567b4ff1)([#1166](https://github.com/tauri-apps/plugins-workspace/pull/1166)) **Breaking change:** Removed the `default-tls` feature flag. The `rustls-tls`, `http2`, `macos-system-configuration`, and `charset` feature flags are now enabled by default. +- [`e3d41f4`](https://github.com/tauri-apps/plugins-workspace/commit/e3d41f4011bd3ea3ce281bb38bbe31d3709f8e0f)([#1191](https://github.com/tauri-apps/plugins-workspace/pull/1191)) Internally use the webview scoped resources table instead of the app one, so other webviews can't access other webviews resources. +- [`7e2fcc5`](https://github.com/tauri-apps/plugins-workspace/commit/7e2fcc5e74df7c3c718e40f75bfb0eafc7d69d8d)([#1146](https://github.com/tauri-apps/plugins-workspace/pull/1146)) Update dependencies to align with tauri 2.0.0-beta.14. +- [`e3d41f4`](https://github.com/tauri-apps/plugins-workspace/commit/e3d41f4011bd3ea3ce281bb38bbe31d3709f8e0f)([#1191](https://github.com/tauri-apps/plugins-workspace/pull/1191)) Update for tauri 2.0.0-beta.15. + +### Dependencies + +- Upgraded to `fs@2.0.0-beta.5` + +## \[2.0.0-beta.4] + +### Dependencies + +- Upgraded to `fs@2.0.0-beta.4` + +## \[2.0.0-beta.3] + +- [`c873e4d`](https://github.com/tauri-apps/plugins-workspace/commit/c873e4d6c74e759742f7c9a88e35cff10a75122a)([#1059](https://github.com/tauri-apps/plugins-workspace/pull/1059)) Fixes scope not allowing subpaths, query parameters and hash when those values are empty. +- [`a04ea2f`](https://github.com/tauri-apps/plugins-workspace/commit/a04ea2f38294d5a3987578283badc8eec87a7752)([#1071](https://github.com/tauri-apps/plugins-workspace/pull/1071)) The global API script is now only added to the binary when the `withGlobalTauri` config is true. +- [`753c7be`](https://github.com/tauri-apps/plugins-workspace/commit/753c7be0a6a78121d2e88ea0efc3040580c885b4)([#1050](https://github.com/tauri-apps/plugins-workspace/pull/1050)) Add `unsafe-headers` cargo feature flag to allow using [forbidden headers](https://fetch.spec.whatwg.org/#terminology-headers). + +### Dependencies + +- Upgraded to `fs@2.0.0-beta.3` + +## \[2.0.0-beta.2] + +- [`ae56b13`](https://github.com/tauri-apps/plugins-workspace/commit/ae56b13a4d49dbf922b8a0fbb0d557bb63c1d72b)([#983](https://github.com/tauri-apps/plugins-workspace/pull/983)) Allow `User-Agent` header to be set. +- [`99bea25`](https://github.com/tauri-apps/plugins-workspace/commit/99bea2559c2c0648c2519c50a18cd124dacef57b)([#1005](https://github.com/tauri-apps/plugins-workspace/pull/1005)) Update to tauri beta.8. + +## \[2.0.0-beta.1] + +- [`569defb`](https://github.com/tauri-apps/plugins-workspace/commit/569defbe9492e38938554bb7bdc1be9151456d21) Update to tauri beta.4. + +## \[2.0.0-beta.0] + +- [`d198c01`](https://github.com/tauri-apps/plugins-workspace/commit/d198c014863ee260cb0de88a14b7fc4356ef7474)([#862](https://github.com/tauri-apps/plugins-workspace/pull/862)) Update to tauri beta. +- [`1a34720`](https://github.com/tauri-apps/plugins-workspace/commit/1a347203a54eccc954749d11c4ee81fdd9a0cde7)([#858](https://github.com/tauri-apps/plugins-workspace/pull/858)) Fix http fetch client option init with parameter `connectTimeout` + +## \[2.0.0-alpha.9] + +### Dependencies + +- Upgraded to `fs@2.0.0-alpha.7` + +## \[2.0.0-alpha.6] + +- [`bfa87da`](https://github.com/tauri-apps/plugins-workspace/commit/bfa87da848f9f1da2abae3354eed632881eddf11)([#824](https://github.com/tauri-apps/plugins-workspace/pull/824)) Add `proxy` field to `fetch` options to configure proxy. + +## \[2.0.0-alpha.5] + +- [`387c2f9`](https://github.com/tauri-apps/plugins-workspace/commit/387c2f9e0ce4c75c07ffa3fd76391a25b58f5daf)([#802](https://github.com/tauri-apps/plugins-workspace/pull/802)) Update to @tauri-apps/api v2.0.0-alpha.13. + +## \[2.0.0-alpha.4] + +- [`387c2f9`](https://github.com/tauri-apps/plugins-workspace/commit/387c2f9e0ce4c75c07ffa3fd76391a25b58f5daf)([#802](https://github.com/tauri-apps/plugins-workspace/pull/802)) Update to @tauri-apps/api v2.0.0-alpha.12. + +## \[2.0.0-alpha.3] + +- [`e438e0a`](https://github.com/tauri-apps/plugins-workspace/commit/e438e0a62d4b430a5159f05f13ecd397dd891a0d)([#676](https://github.com/tauri-apps/plugins-workspace/pull/676)) Update to @tauri-apps/api v2.0.0-alpha.11. + +## \[2.0.0-alpha.2] + +- [`5c13736`](https://github.com/tauri-apps/plugins-workspace/commit/5c137365c60790e8d4037d449e8237aa3fffdab0)([#673](https://github.com/tauri-apps/plugins-workspace/pull/673)) Update to @tauri-apps/api v2.0.0-alpha.9. + +## \[2.0.0-alpha.3] + +- [`2cb0fa7`](https://github.com/tauri-apps/plugins-workspace/commit/2cb0fa719b8b1f5ac07dada93520dbbcf637d64c)([#587](https://github.com/tauri-apps/plugins-workspace/pull/587)) Remove `cmd` property which breaks invoke call. +- [`4e2cef9`](https://github.com/tauri-apps/plugins-workspace/commit/4e2cef9b702bbbb9cf4ee17de50791cb21f1b2a4)([#593](https://github.com/tauri-apps/plugins-workspace/pull/593)) Update to alpha.12. + +### Dependencies + +- Upgraded to `fs@2.0.0-alpha.2` + +## \[2.0.0-alpha.2] + +- [`aec17a9`](https://github.com/tauri-apps/plugins-workspace/commit/aec17a90fc365774c70c4876b94a899416120e26)([#558](https://github.com/tauri-apps/plugins-workspace/pull/558)) Improve response performance by using the new IPC streaming data. + +## \[2.0.0-alpha.1] + +- [`7d9df72`](https://github.com/tauri-apps/plugins-workspace/commit/7d9df7297a221a64d9de945ffc2cd8313d3104dc)([#428](https://github.com/tauri-apps/plugins-workspace/pull/428)) Multipart requests are now handled in JavaScript by the `Request` JavaScript class so you just need to use a `FormData` body and not set the content-type header to `multipart/form-data`. `application/x-www-form-urlencoded` requests must be done manually. +- [`7d9df72`](https://github.com/tauri-apps/plugins-workspace/commit/7d9df7297a221a64d9de945ffc2cd8313d3104dc)([#428](https://github.com/tauri-apps/plugins-workspace/pull/428)) The http plugin has been rewritten from scratch and now only exposes a `fetch` function in Javascript and Re-exports `reqwest` crate in Rust. The new `fetch` method tries to be as close and compliant to the `fetch` Web API as possible. +- [`d74fc0a`](https://github.com/tauri-apps/plugins-workspace/commit/d74fc0a097996e90a37be8f57d50b7d1f6ca616f)([#555](https://github.com/tauri-apps/plugins-workspace/pull/555)) Update to alpha.11. + +## \[2.0.0-alpha.0] + +- [`717ae67`](https://github.com/tauri-apps/plugins-workspace/commit/717ae670978feb4492fac1f295998b93f2b9347f)([#371](https://github.com/tauri-apps/plugins-workspace/pull/371)) First v2 alpha release! diff --git a/plugins/http/Cargo.toml b/plugins/http/Cargo.toml new file mode 100644 index 0000000..f0eeadd --- /dev/null +++ b/plugins/http/Cargo.toml @@ -0,0 +1,80 @@ +[package] +name = "tauri-plugin-http" +version = "2.7.0" +description = "Access an HTTP client written in Rust." +edition = { workspace = true } +authors = { workspace = true } +license = { workspace = true } +rust-version = { workspace = true } +repository = { workspace = true } +links = "tauri-plugin-http" +exclude = [ + "/banner.png", + "/guest-js", + "/package.json", + "/rollup.config.js", + "/tsconfig.json", +] + +[package.metadata.platforms.support] +windows = { level = "full", notes = "" } +linux = { level = "full", notes = "" } +macos = { level = "full", notes = "" } +android = { level = "full", notes = "" } +ios = { level = "full", notes = "" } + +[build-dependencies] +tauri-plugin = { workspace = true, features = ["build"] } +schemars = { workspace = true } +serde = { workspace = true } +url = { workspace = true } +urlpattern = "0.3" +regex = "1" + +[dependencies] +serde = { workspace = true } +serde_json = { workspace = true } +tauri = { workspace = true } +thiserror = { workspace = true } +tokio = { version = "1", features = ["sync", "macros"] } +tauri-plugin-fs = { path = "../fs", version = "2.5.2" } +urlpattern = "0.3" +regex = "1" +http = "1" +# 0.12.16 is when `system-proxy` was added +reqwest = { version = "0.12.16", default-features = false } +url = { workspace = true } +data-url = "0.3" +cookie_store = { version = "0.22", optional = true, features = ["serde"] } +bytes = { version = "1.9", optional = true } +tracing = { workspace = true, optional = true } + +[features] +default = ["rustls-tls", "http2", "charset", "system-proxy", "cookies"] +multipart = ["reqwest/multipart"] +json = ["reqwest/json"] +stream = ["reqwest/stream"] +native-tls = ["reqwest/native-tls"] +native-tls-vendored = ["reqwest/native-tls-vendored"] +native-tls-alpn = ["reqwest/native-tls-alpn"] +rustls-tls = ["reqwest/rustls-tls"] +rustls-tls-manual-roots = ["reqwest/rustls-tls-manual-roots"] +rustls-tls-webpki-roots = ["reqwest/rustls-tls-webpki-roots"] +rustls-tls-native-roots = ["reqwest/rustls-tls-native-roots"] +blocking = ["reqwest/blocking"] +cookies = ["reqwest/cookies", "dep:cookie_store", "dep:bytes"] +gzip = ["reqwest/gzip"] +brotli = ["reqwest/brotli"] +deflate = ["reqwest/deflate"] +zstd = ["reqwest/zstd"] +trust-dns = ["reqwest/trust-dns"] +socks = ["reqwest/socks"] +http2 = ["reqwest/http2"] +charset = ["reqwest/charset"] +# Deprecated, switch to system-proxy +macos-system-configuration = ["reqwest/macos-system-configuration"] +system-proxy = ["reqwest/system-proxy"] + +unsafe-headers = [] +tracing = ["dep:tracing"] +dangerous-settings = [] diff --git a/plugins/http/LICENSE.spdx b/plugins/http/LICENSE.spdx new file mode 100644 index 0000000..cdd0df5 --- /dev/null +++ b/plugins/http/LICENSE.spdx @@ -0,0 +1,20 @@ +SPDXVersion: SPDX-2.1 +DataLicense: CC0-1.0 +PackageName: tauri +DataFormat: SPDXRef-1 +PackageSupplier: Organization: The Tauri Programme in the Commons Conservancy +PackageHomePage: https://tauri.app +PackageLicenseDeclared: Apache-2.0 +PackageLicenseDeclared: MIT +PackageCopyrightText: 2019-2022, The Tauri Programme in the Commons Conservancy +PackageSummary: Tauri is a rust project that enables developers to make secure +and small desktop applications using a web frontend. + +PackageComment: The package includes the following libraries; see +Relationship information. + +Created: 2019-05-20T09:00:00Z +PackageDownloadLocation: git://github.com/tauri-apps/tauri +PackageDownloadLocation: git+https://github.com/tauri-apps/tauri.git +PackageDownloadLocation: git+ssh://github.com/tauri-apps/tauri.git +Creator: Person: Daniel Thompson-Yvetot \ No newline at end of file diff --git a/plugins/http/LICENSE_APACHE-2.0 b/plugins/http/LICENSE_APACHE-2.0 new file mode 100644 index 0000000..4947287 --- /dev/null +++ b/plugins/http/LICENSE_APACHE-2.0 @@ -0,0 +1,177 @@ + + 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 \ No newline at end of file diff --git a/plugins/http/LICENSE_MIT b/plugins/http/LICENSE_MIT new file mode 100644 index 0000000..4d75472 --- /dev/null +++ b/plugins/http/LICENSE_MIT @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2017 - Present Tauri Apps 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. \ No newline at end of file diff --git a/plugins/http/README.md b/plugins/http/README.md new file mode 100644 index 0000000..7b8e3d2 --- /dev/null +++ b/plugins/http/README.md @@ -0,0 +1,93 @@ +![plugin-http](https://github.com/tauri-apps/plugins-workspace/raw/v2/plugins/http/banner.png) + +Access the HTTP client written in Rust. + +| Platform | Supported | +| -------- | --------- | +| Linux | ✓ | +| Windows | ✓ | +| macOS | ✓ | +| Android | ✓ | +| iOS | ✓ | + +## Install + +_This plugin requires a Rust version of at least **1.77.2**_ + +There are three general methods of installation that we can recommend. + +1. Use crates.io and npm (easiest, and requires you to trust that our publishing pipeline worked) +2. Pull sources directly from Github using git tags / revision hashes (most secure) +3. Git submodule install this repo in your tauri project and then use file protocol to ingest the source (most secure, but inconvenient to use) + +Install the Core plugin by adding the following to your `Cargo.toml` file: + +`src-tauri/Cargo.toml` + +```toml +[dependencies] +tauri-plugin-http = "2.0.0" +# alternatively with Git: +tauri-plugin-http = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v2" } +``` + +You can install the JavaScript Guest bindings using your preferred JavaScript package manager: + +```sh +pnpm add @tauri-apps/plugin-http +# or +npm add @tauri-apps/plugin-http +# or +yarn add @tauri-apps/plugin-http +``` + +## Usage + +First you need to register the core plugin with Tauri: + +`src-tauri/src/lib.rs` + +```rust +fn main() { + tauri::Builder::default() + .plugin(tauri_plugin_http::init()) + .run(tauri::generate_context!()) + .expect("error while running tauri application"); +} +``` + +Afterwards all the plugin's APIs are available through the JavaScript guest bindings: + +```javascript +import { fetch } from '@tauri-apps/plugin-http' +const response = await fetch('http://localhost:3003/users/2', { + method: 'GET', + connectTimeout: 30 +}) +``` + +## Contributing + +PRs accepted. Please make sure to read the Contributing Guide before making a pull request. + +## Partners + + + + + + + +
+ + CrabNebula + +
+ +For the complete list of sponsors please visit our [website](https://tauri.app#sponsors) and [Open Collective](https://opencollective.com/tauri). + +## License + +Code: (c) 2015 - Present - The Tauri Programme within The Commons Conservancy. + +MIT or MIT/Apache 2.0 where applicable. diff --git a/plugins/http/SECURITY.md b/plugins/http/SECURITY.md new file mode 100644 index 0000000..4f09bba --- /dev/null +++ b/plugins/http/SECURITY.md @@ -0,0 +1,23 @@ +# Security Policy + +**Do not report security vulnerabilities through public GitHub issues.** + +**Please use the [Private Vulnerability Disclosure](https://docs.github.com/en/code-security/security-advisories/guidance-on-reporting-and-writing-information-about-vulnerabilities/privately-reporting-a-security-vulnerability#privately-reporting-a-security-vulnerability) feature of GitHub.** + +Include as much of the following information: + +- Type of issue (e.g. improper input parsing, privilege escalation, etc.) +- The location of the affected source code (tag/branch/commit or direct URL) +- Any special configuration required to reproduce the issue +- The distribution affected or used to help us with reproduction of the issue +- Step-by-step instructions to reproduce the issue +- Ideally a reproduction repository +- Impact of the issue, including how an attacker might exploit the issue + +We prefer to receive reports in English. + +## Contact + +Please disclose a vulnerability or security relevant issue here: [https://github.com/tauri-apps/plugins-workspace/security/advisories/new](https://github.com/tauri-apps/plugins-workspace/security/advisories/new). + +Alternatively, you can also contact us by email via [security@tauri.app](mailto:security@tauri.app). diff --git a/plugins/http/api-iife.js b/plugins/http/api-iife.js new file mode 100644 index 0000000..5d3720a --- /dev/null +++ b/plugins/http/api-iife.js @@ -0,0 +1 @@ +if("__TAURI__"in window){var __TAURI_PLUGIN_HTTP__=function(e,t){"use strict";const r="Request cancelled";return e.fetch=async function(e,n){const a=n?.signal;if(a?.aborted)throw new Error(r);const o=n?.maxRedirections,i=n?.connectTimeout,s=n?.proxy,c=n?.danger;n&&(delete n.maxRedirections,delete n.connectTimeout,delete n.proxy,delete n.danger);const d=n?.headers?n.headers instanceof Headers?n.headers:new Headers(n.headers):new Headers,l=new Request(e,n),u=await l.arrayBuffer(),h=0!==u.byteLength?Array.from(new Uint8Array(u)):null;for(const[e,t]of l.headers)d.get(e)||d.set(e,t);const _=(d instanceof Headers?Array.from(d.entries()):Array.isArray(d)?d:Object.entries(d)).map(([e,t])=>[e,"string"==typeof t?t:t.toString()]);if(a?.aborted)throw new Error(r);const w=await t.invoke("plugin:http|fetch",{clientConfig:{method:l.method,url:l.url,headers:_,data:h,maxRedirections:o,connectTimeout:i,proxy:s,danger:c}}),f=()=>t.invoke("plugin:http|fetch_cancel",{rid:w}).catch(()=>{});if(a?.aborted)throw f(),new Error(r);a?.addEventListener("abort",()=>{f()});const{status:y,statusText:p,url:b,headers:g,rid:v}=await t.invoke("plugin:http|fetch_send",{rid:w});let T=!1;const m=()=>T?Promise.resolve():(T=!0,t.invoke("plugin:http|fetch_cancel_body",{rid:v}).catch(()=>{})),R=[101,103,204,205,304].includes(y)?null:new ReadableStream({start:e=>{a?.addEventListener("abort",()=>{e.error(r),m()})},pull:e=>(async e=>{let r;try{r=await t.invoke("plugin:http|fetch_read_body",{rid:v})}catch(t){return e.error(t),void m()}const n=new Uint8Array(r),a=n[n.byteLength-1],o=n.slice(0,n.byteLength-1);1!==a?e.enqueue(o):e.close()})(e),cancel:()=>{m()}}),A=new Response(R,{status:y,statusText:p});Object.defineProperty(A,"url",{value:b,writable:!1}),Object.defineProperty(A,"headers",{value:new Headers(g),writable:!1});const P=A.clone.bind(A);return Object.defineProperty(A,"clone",{value:()=>{const e=P();return Object.defineProperty(e,"url",{value:b,writable:!1}),Object.defineProperty(e,"headers",{value:new Headers(g),writable:!1}),e}}),A},e}({},window.__TAURI__.core);Object.defineProperty(window.__TAURI__,"http",{value:__TAURI_PLUGIN_HTTP__})} diff --git a/plugins/http/banner.png b/plugins/http/banner.png new file mode 100644 index 0000000000000000000000000000000000000000..121741e51b22a81b4dda958df3821989054a0197 GIT binary patch literal 52113 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 literal 0 HcmV?d00001 diff --git a/plugins/http/build.rs b/plugins/http/build.rs new file mode 100644 index 0000000..31fa023 --- /dev/null +++ b/plugins/http/build.rs @@ -0,0 +1,73 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +#[path = "src/scope.rs"] +#[allow(dead_code)] +mod scope; + +const COMMANDS: &[&str] = &[ + "fetch", + "fetch_cancel", + "fetch_send", + "fetch_read_body", + "fetch_cancel_body", +]; + +/// HTTP scope entry. +#[derive(schemars::JsonSchema)] +#[serde(untagged)] +#[allow(unused)] +enum HttpScopeEntry { + /// A URL that can be accessed by the webview when using the HTTP APIs. + /// Wildcards can be used following the URL pattern standard. + /// + /// See [the URL Pattern spec](https://urlpattern.spec.whatwg.org/) for more information. + /// + /// Examples: + /// + /// - "https://*" : allows all HTTPS origin on port 443 + /// + /// - "https://*:*" : allows all HTTPS origin on any port + /// + /// - "https://*.github.com/tauri-apps/tauri": allows any subdomain of "github.com" with the "tauri-apps/api" path + /// + /// - "https://myapi.service.com/users/*": allows access to any URLs that begins with "https://myapi.service.com/users/" + Value(String), + Object { + /// A URL that can be accessed by the webview when using the HTTP APIs. + /// Wildcards can be used following the URL pattern standard. + /// + /// See [the URL Pattern spec](https://urlpattern.spec.whatwg.org/) for more information. + /// + /// Examples: + /// + /// - "https://*" : allows all HTTPS origin on port 443 + /// + /// - "https://*:*" : allows all HTTPS origin on any port + /// + /// - "https://*.github.com/tauri-apps/tauri": allows any subdomain of "github.com" with the "tauri-apps/api" path + /// + /// - "https://myapi.service.com/users/*": allows access to any URLs that begins with "https://myapi.service.com/users/" + url: String, + }, +} + +// Ensure `HttpScopeEntry` and `scope::EntryRaw` is kept in sync +fn _f() { + match scope::EntryRaw::Value(String::new()) { + scope::EntryRaw::Value(url) => HttpScopeEntry::Value(url), + scope::EntryRaw::Object { url } => HttpScopeEntry::Object { url }, + }; + match HttpScopeEntry::Value(String::new()) { + HttpScopeEntry::Value(url) => scope::EntryRaw::Value(url), + HttpScopeEntry::Object { url } => scope::EntryRaw::Object { url }, + }; +} + +fn main() { + tauri_plugin::Builder::new(COMMANDS) + .global_api_script_path("./api-iife.js") + .global_scope_schema(schemars::schema_for!(HttpScopeEntry)) + .build(); +} diff --git a/plugins/http/guest-js/index.ts b/plugins/http/guest-js/index.ts new file mode 100644 index 0000000..5d4e094 --- /dev/null +++ b/plugins/http/guest-js/index.ts @@ -0,0 +1,347 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +/** + * Make HTTP requests with the Rust backend. + * + * ## Security + * + * This API has a scope configuration that forces you to restrict the URLs that can be accessed using glob patterns. + * + * For instance, this scope configuration only allows making HTTP requests to all subdomains for `tauri.app` except for `https://private.tauri.app`: + * ```json + * { + * "permissions": [ + * { + * "identifier": "http:default", + * "allow": [{ "url": "https://*.tauri.app" }], + * "deny": [{ "url": "https://private.tauri.app" }] + * } + * ] + * } + * ``` + * Trying to execute any API with a URL not configured on the scope results in a promise rejection due to denied access. + * + * @module + */ + +import { invoke } from '@tauri-apps/api/core' + +/** + * Configuration of a proxy that a Client should pass requests to. + * + * @since 2.0.0 + */ +export interface Proxy { + /** + * Proxy all traffic to the passed URL. + */ + all?: string | ProxyConfig + /** + * Proxy all HTTP traffic to the passed URL. + */ + http?: string | ProxyConfig + /** + * Proxy all HTTPS traffic to the passed URL. + */ + https?: string | ProxyConfig +} + +/** + * Detailed configuration of a single proxy server, used when a plain URL string is not enough. + * + * @since 2.0.0 + */ +export interface ProxyConfig { + /** + * The URL of the proxy server. + */ + url: string + /** + * Set the `Proxy-Authorization` header using Basic auth. + */ + basicAuth?: { + /** + * The user name sent to the proxy server. + */ + username: string + /** + * The password sent to the proxy server. + */ + password: string + } + /** + * A configuration for filtering out requests that shouldn't be proxied. + * Entries are expected to be comma-separated (whitespace between entries is ignored) + */ + noProxy?: string +} + +/** + * Options to configure the Rust client used to make fetch requests + * + * @since 2.0.0 + */ +export interface ClientOptions { + /** + * Defines the maximum number of redirects the client should follow. + * If set to 0, no redirects will be followed. + * + * When the `scopeRedirects` plugin configuration is enabled, every redirect must + * also be allowed by the configured scope, otherwise the request fails + * instead of being followed. + */ + maxRedirections?: number + /** Timeout in milliseconds */ + connectTimeout?: number + /** + * Configuration of a proxy that a Client should pass requests to. + */ + proxy?: Proxy + /** + * Configuration for dangerous settings on the client such as disabling SSL verification. + */ + danger?: DangerousSettings +} + +/** + * Configuration for dangerous settings on the client such as disabling SSL verification. + * + * @since 2.3.0 + */ +export interface DangerousSettings { + /** + * Disables SSL verification. + */ + acceptInvalidCerts?: boolean + /** + * Disables hostname verification. + */ + acceptInvalidHostnames?: boolean +} + +const ERROR_REQUEST_CANCELLED = 'Request cancelled' + +/** + * Fetch a resource from the network. It returns a `Promise` that resolves to the + * `Response` to that `Request`, whether it is successful or not. + * + * The request is performed by the Rust backend instead of the webview, so it is not subject to + * CORS, but the URL must be allowed by the plugin scope. + * + * @example + * ```typescript + * import { fetch } from '@tauri-apps/plugin-http'; + * const response = await fetch("http://my.json.host/data.json"); + * console.log(response.status); // e.g. 200 + * console.log(response.statusText); // e.g. "OK" + * const jsonData = await response.json(); + * ``` + * + * @param input The resource to fetch, as a URL, a string or a `Request` object. + * @param init The standard `fetch` request options, extended with the Rust client options from + * {@linkcode ClientOptions}: `maxRedirections`, `connectTimeout`, `proxy` and `danger`. The + * `signal` option can be used to abort the request. + * @returns A promise resolving to the `Response` of the request. + * + * @since 2.0.0 + */ +export async function fetch( + input: URL | Request | string, + init?: RequestInit & ClientOptions +): Promise { + // Optimistically check for abort signal and avoid doing any work + const signal = init?.signal + if (signal?.aborted) { + throw new Error(ERROR_REQUEST_CANCELLED) + } + + const maxRedirections = init?.maxRedirections + const connectTimeout = init?.connectTimeout + const proxy = init?.proxy + const danger = init?.danger + + // Remove these fields before creating the request + if (init) { + delete init.maxRedirections + delete init.connectTimeout + delete init.proxy + delete init.danger + } + + const headers = init?.headers + ? init.headers instanceof Headers + ? init.headers + : new Headers(init.headers) + : new Headers() + + const req = new Request(input, init) + const buffer = await req.arrayBuffer() + const data = + buffer.byteLength !== 0 ? Array.from(new Uint8Array(buffer)) : null + + // append new headers created by the browser `Request` implementation, + // if not already declared by the caller of this function + for (const [key, value] of req.headers) { + if (!headers.get(key)) { + headers.set(key, value) + } + } + + const headersArray = + headers instanceof Headers + ? Array.from(headers.entries()) + : Array.isArray(headers) + ? headers + : Object.entries(headers) + + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + const mappedHeaders: Array<[string, string]> = headersArray.map( + ([name, val]) => [ + name, + // we need to ensure we have all header values as strings + // eslint-disable-next-line + typeof val === 'string' ? val : (val as any).toString() + ] + ) + + // Optimistically check for abort signal and avoid doing any work on the Rust side + if (signal?.aborted) { + throw new Error(ERROR_REQUEST_CANCELLED) + } + + const rid = await invoke('plugin:http|fetch', { + clientConfig: { + method: req.method, + url: req.url, + headers: mappedHeaders, + data, + maxRedirections, + connectTimeout, + proxy, + danger + } + }) + + const abort = () => + invoke('plugin:http|fetch_cancel', { rid }).catch(() => {}) + + // Optimistically check for abort signal + // and avoid doing any work after doing intial work on the Rust side + if (signal?.aborted) { + // we don't care about the result of this promise + void abort() + throw new Error(ERROR_REQUEST_CANCELLED) + } + + signal?.addEventListener('abort', () => void abort()) + + interface FetchSendResponse { + status: number + statusText: string + headers: [[string, string]] + url: string + rid: number + } + + const { + status, + statusText, + url, + headers: responseHeaders, + rid: responseRid + } = await invoke('plugin:http|fetch_send', { + rid + }) + + let bodyDropped = false + const dropBody = () => { + if (bodyDropped) return Promise.resolve() + bodyDropped = true + return invoke('plugin:http|fetch_cancel_body', { rid: responseRid }).catch( + () => {} + ) + } + + const readChunk = async ( + controller: ReadableStreamDefaultController + ) => { + let data: ArrayBuffer + try { + data = await invoke('plugin:http|fetch_read_body', { + rid: responseRid + }) + } catch (e) { + // close the stream if an error occurs + // and drop the body on Rust side + controller.error(e) + void dropBody() + return + } + + const dataUint8 = new Uint8Array(data) + const lastByte = dataUint8[dataUint8.byteLength - 1] + const actualData = dataUint8.slice(0, dataUint8.byteLength - 1) + + // close when the signal to close (last byte is 1) is sent from the IPC. + if (lastByte === 1) { + controller.close() + return + } + + controller.enqueue(actualData) + } + + // no body for 101, 103, 204, 205 and 304 + // see https://fetch.spec.whatwg.org/#null-body-status + const body = [101, 103, 204, 205, 304].includes(status) + ? null + : new ReadableStream({ + start: (controller) => { + // listen for abort events to cancel reading + signal?.addEventListener('abort', () => { + controller.error(ERROR_REQUEST_CANCELLED) + void dropBody() + }) + }, + pull: (controller) => readChunk(controller), + cancel: () => { + // Ensure body resources are released on stream cancellation + void dropBody() + } + }) + + const res = new Response(body, { + status, + statusText + }) + + // `Response.url` cannot be set via the constructor, so we define it manually + Object.defineProperty(res, 'url', { value: url, writable: false }) + + // Expose `set-cookie` via `response.headers` (and `getSetCookie()` where + // supported). This is not Fetch-spec compliant for network responses in + // browsers, where `set-cookie` is treated as a forbidden response + // header and is generally not readable from JavaScript. + Object.defineProperty(res, 'headers', { + value: new Headers(responseHeaders), + writable: false + }) + + // Patch clone() per-instance so cloning preserves the overridden properties + const originalClone = res.clone.bind(res) + Object.defineProperty(res, 'clone', { + value: () => { + const cloned = originalClone() + Object.defineProperty(cloned, 'url', { value: url, writable: false }) + Object.defineProperty(cloned, 'headers', { + value: new Headers(responseHeaders), + writable: false + }) + return cloned + } + }) + + return res +} diff --git a/plugins/http/package.json b/plugins/http/package.json new file mode 100644 index 0000000..5d612e6 --- /dev/null +++ b/plugins/http/package.json @@ -0,0 +1,29 @@ +{ + "name": "@tauri-apps/plugin-http", + "version": "2.6.1", + "license": "MIT OR Apache-2.0", + "authors": [ + "Tauri Programme within The Commons Conservancy" + ], + "repository": "https://github.com/tauri-apps/plugins-workspace", + "type": "module", + "types": "./dist-js/index.d.ts", + "main": "./dist-js/index.cjs", + "module": "./dist-js/index.js", + "exports": { + "types": "./dist-js/index.d.ts", + "import": "./dist-js/index.js", + "require": "./dist-js/index.cjs" + }, + "scripts": { + "build": "rollup -c" + }, + "files": [ + "dist-js", + "README.md", + "LICENSE" + ], + "dependencies": { + "@tauri-apps/api": "^2.11.0" + } +} diff --git a/plugins/http/permissions/autogenerated/commands/fetch.toml b/plugins/http/permissions/autogenerated/commands/fetch.toml new file mode 100644 index 0000000..c4e068a --- /dev/null +++ b/plugins/http/permissions/autogenerated/commands/fetch.toml @@ -0,0 +1,13 @@ +# Automatically generated - DO NOT EDIT! + +"$schema" = "../../schemas/schema.json" + +[[permission]] +identifier = "allow-fetch" +description = "Enables the fetch command without any pre-configured scope." +commands.allow = ["fetch"] + +[[permission]] +identifier = "deny-fetch" +description = "Denies the fetch command without any pre-configured scope." +commands.deny = ["fetch"] diff --git a/plugins/http/permissions/autogenerated/commands/fetch_cancel.toml b/plugins/http/permissions/autogenerated/commands/fetch_cancel.toml new file mode 100644 index 0000000..70c1139 --- /dev/null +++ b/plugins/http/permissions/autogenerated/commands/fetch_cancel.toml @@ -0,0 +1,13 @@ +# Automatically generated - DO NOT EDIT! + +"$schema" = "../../schemas/schema.json" + +[[permission]] +identifier = "allow-fetch-cancel" +description = "Enables the fetch_cancel command without any pre-configured scope." +commands.allow = ["fetch_cancel"] + +[[permission]] +identifier = "deny-fetch-cancel" +description = "Denies the fetch_cancel command without any pre-configured scope." +commands.deny = ["fetch_cancel"] diff --git a/plugins/http/permissions/autogenerated/commands/fetch_cancel_body.toml b/plugins/http/permissions/autogenerated/commands/fetch_cancel_body.toml new file mode 100644 index 0000000..19e6652 --- /dev/null +++ b/plugins/http/permissions/autogenerated/commands/fetch_cancel_body.toml @@ -0,0 +1,13 @@ +# Automatically generated - DO NOT EDIT! + +"$schema" = "../../schemas/schema.json" + +[[permission]] +identifier = "allow-fetch-cancel-body" +description = "Enables the fetch_cancel_body command without any pre-configured scope." +commands.allow = ["fetch_cancel_body"] + +[[permission]] +identifier = "deny-fetch-cancel-body" +description = "Denies the fetch_cancel_body command without any pre-configured scope." +commands.deny = ["fetch_cancel_body"] diff --git a/plugins/http/permissions/autogenerated/commands/fetch_read_body.toml b/plugins/http/permissions/autogenerated/commands/fetch_read_body.toml new file mode 100644 index 0000000..b5867e9 --- /dev/null +++ b/plugins/http/permissions/autogenerated/commands/fetch_read_body.toml @@ -0,0 +1,13 @@ +# Automatically generated - DO NOT EDIT! + +"$schema" = "../../schemas/schema.json" + +[[permission]] +identifier = "allow-fetch-read-body" +description = "Enables the fetch_read_body command without any pre-configured scope." +commands.allow = ["fetch_read_body"] + +[[permission]] +identifier = "deny-fetch-read-body" +description = "Denies the fetch_read_body command without any pre-configured scope." +commands.deny = ["fetch_read_body"] diff --git a/plugins/http/permissions/autogenerated/commands/fetch_send.toml b/plugins/http/permissions/autogenerated/commands/fetch_send.toml new file mode 100644 index 0000000..f6a55d2 --- /dev/null +++ b/plugins/http/permissions/autogenerated/commands/fetch_send.toml @@ -0,0 +1,13 @@ +# Automatically generated - DO NOT EDIT! + +"$schema" = "../../schemas/schema.json" + +[[permission]] +identifier = "allow-fetch-send" +description = "Enables the fetch_send command without any pre-configured scope." +commands.allow = ["fetch_send"] + +[[permission]] +identifier = "deny-fetch-send" +description = "Denies the fetch_send command without any pre-configured scope." +commands.deny = ["fetch_send"] diff --git a/plugins/http/permissions/autogenerated/reference.md b/plugins/http/permissions/autogenerated/reference.md new file mode 100644 index 0000000..2f39e74 --- /dev/null +++ b/plugins/http/permissions/autogenerated/reference.md @@ -0,0 +1,160 @@ +## Default Permission + +This permission set configures what kind of +fetch operations are available from the http plugin. + +This enables all fetch operations but does not +allow explicitly any origins to be fetched. This needs to +be manually configured before usage. + +#### Granted Permissions + +All fetch operations are enabled. + +#### This default permission set includes the following: + +- `allow-fetch` +- `allow-fetch-cancel` +- `allow-fetch-send` +- `allow-fetch-read-body` +- `allow-fetch-cancel-body` + +## Permission Table + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
IdentifierDescription
+ +`http:allow-fetch` + + + +Enables the fetch command without any pre-configured scope. + +
+ +`http:deny-fetch` + + + +Denies the fetch command without any pre-configured scope. + +
+ +`http:allow-fetch-cancel` + + + +Enables the fetch_cancel command without any pre-configured scope. + +
+ +`http:deny-fetch-cancel` + + + +Denies the fetch_cancel command without any pre-configured scope. + +
+ +`http:allow-fetch-cancel-body` + + + +Enables the fetch_cancel_body command without any pre-configured scope. + +
+ +`http:deny-fetch-cancel-body` + + + +Denies the fetch_cancel_body command without any pre-configured scope. + +
+ +`http:allow-fetch-read-body` + + + +Enables the fetch_read_body command without any pre-configured scope. + +
+ +`http:deny-fetch-read-body` + + + +Denies the fetch_read_body command without any pre-configured scope. + +
+ +`http:allow-fetch-send` + + + +Enables the fetch_send command without any pre-configured scope. + +
+ +`http:deny-fetch-send` + + + +Denies the fetch_send command without any pre-configured scope. + +
diff --git a/plugins/http/permissions/default.toml b/plugins/http/permissions/default.toml new file mode 100644 index 0000000..541bf49 --- /dev/null +++ b/plugins/http/permissions/default.toml @@ -0,0 +1,23 @@ +"$schema" = "schemas/schema.json" + +[default] +description = """ +This permission set configures what kind of +fetch operations are available from the http plugin. + +This enables all fetch operations but does not +allow explicitly any origins to be fetched. This needs to +be manually configured before usage. + +#### Granted Permissions + +All fetch operations are enabled. + +""" +permissions = [ + "allow-fetch", + "allow-fetch-cancel", + "allow-fetch-send", + "allow-fetch-read-body", + "allow-fetch-cancel-body", +] diff --git a/plugins/http/permissions/schemas/schema.json b/plugins/http/permissions/schemas/schema.json new file mode 100644 index 0000000..a6b457b --- /dev/null +++ b/plugins/http/permissions/schemas/schema.json @@ -0,0 +1,366 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "PermissionFile", + "description": "Permission file that can define a default permission, a set of permissions or a list of inlined permissions.", + "type": "object", + "properties": { + "default": { + "description": "The default permission set for the plugin", + "anyOf": [ + { + "$ref": "#/definitions/DefaultPermission" + }, + { + "type": "null" + } + ] + }, + "set": { + "description": "A list of permissions sets defined", + "type": "array", + "items": { + "$ref": "#/definitions/PermissionSet" + } + }, + "permission": { + "description": "A list of inlined permissions", + "default": [], + "type": "array", + "items": { + "$ref": "#/definitions/Permission" + } + } + }, + "definitions": { + "DefaultPermission": { + "description": "The default permission set of the plugin.\n\nWorks similarly to a permission with the \"default\" identifier.", + "type": "object", + "required": [ + "permissions" + ], + "properties": { + "version": { + "description": "The version of the permission.", + "type": [ + "integer", + "null" + ], + "format": "uint64", + "minimum": 1.0 + }, + "description": { + "description": "Human-readable description of what the permission does. Tauri convention is to use `

` headings in markdown content for Tauri documentation generation purposes.", + "type": [ + "string", + "null" + ] + }, + "permissions": { + "description": "All permissions this set contains.", + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "PermissionSet": { + "description": "A set of direct permissions grouped together under a new name.", + "type": "object", + "required": [ + "description", + "identifier", + "permissions" + ], + "properties": { + "identifier": { + "description": "A unique identifier for the permission.", + "type": "string" + }, + "description": { + "description": "Human-readable description of what the permission does.", + "type": "string" + }, + "permissions": { + "description": "All permissions this set contains.", + "type": "array", + "items": { + "$ref": "#/definitions/PermissionKind" + } + } + } + }, + "Permission": { + "description": "Descriptions of explicit privileges of commands.\n\nIt can enable commands to be accessible in the frontend of the application.\n\nIf the scope is defined it can be used to fine grain control the access of individual or multiple commands.", + "type": "object", + "required": [ + "identifier" + ], + "properties": { + "version": { + "description": "The version of the permission.", + "type": [ + "integer", + "null" + ], + "format": "uint64", + "minimum": 1.0 + }, + "identifier": { + "description": "A unique identifier for the permission.", + "type": "string" + }, + "description": { + "description": "Human-readable description of what the permission does. Tauri internal convention is to use `

` headings in markdown content for Tauri documentation generation purposes.", + "type": [ + "string", + "null" + ] + }, + "commands": { + "description": "Allowed or denied commands when using this permission.", + "default": { + "allow": [], + "deny": [] + }, + "allOf": [ + { + "$ref": "#/definitions/Commands" + } + ] + }, + "scope": { + "description": "Allowed or denied scoped when using this permission.", + "allOf": [ + { + "$ref": "#/definitions/Scopes" + } + ] + }, + "platforms": { + "description": "Target platforms this permission applies. By default all platforms are affected by this permission.", + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/Target" + } + } + } + }, + "Commands": { + "description": "Allowed and denied commands inside a permission.\n\nIf two commands clash inside of `allow` and `deny`, it should be denied by default.", + "type": "object", + "properties": { + "allow": { + "description": "Allowed command.", + "default": [], + "type": "array", + "items": { + "type": "string" + } + }, + "deny": { + "description": "Denied command, which takes priority.", + "default": [], + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "Scopes": { + "description": "An argument for fine grained behavior control of Tauri commands.\n\nIt can be of any serde serializable type and is used to allow or prevent certain actions inside a Tauri command. The configured scope is passed to the command and will be enforced by the command implementation.\n\n## Example\n\n```json { \"allow\": [{ \"path\": \"$HOME/**\" }], \"deny\": [{ \"path\": \"$HOME/secret.txt\" }] } ```", + "type": "object", + "properties": { + "allow": { + "description": "Data that defines what is allowed by the scope.", + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/Value" + } + }, + "deny": { + "description": "Data that defines what is denied by the scope. This should be prioritized by validation logic.", + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/Value" + } + } + } + }, + "Value": { + "description": "All supported ACL values.", + "anyOf": [ + { + "description": "Represents a null JSON value.", + "type": "null" + }, + { + "description": "Represents a [`bool`].", + "type": "boolean" + }, + { + "description": "Represents a valid ACL [`Number`].", + "allOf": [ + { + "$ref": "#/definitions/Number" + } + ] + }, + { + "description": "Represents a [`String`].", + "type": "string" + }, + { + "description": "Represents a list of other [`Value`]s.", + "type": "array", + "items": { + "$ref": "#/definitions/Value" + } + }, + { + "description": "Represents a map of [`String`] keys to [`Value`]s.", + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/Value" + } + } + ] + }, + "Number": { + "description": "A valid ACL number.", + "anyOf": [ + { + "description": "Represents an [`i64`].", + "type": "integer", + "format": "int64" + }, + { + "description": "Represents a [`f64`].", + "type": "number", + "format": "double" + } + ] + }, + "Target": { + "description": "Platform target.", + "oneOf": [ + { + "description": "MacOS.", + "type": "string", + "enum": [ + "macOS" + ] + }, + { + "description": "Windows.", + "type": "string", + "enum": [ + "windows" + ] + }, + { + "description": "Linux.", + "type": "string", + "enum": [ + "linux" + ] + }, + { + "description": "Android.", + "type": "string", + "enum": [ + "android" + ] + }, + { + "description": "iOS.", + "type": "string", + "enum": [ + "iOS" + ] + } + ] + }, + "PermissionKind": { + "type": "string", + "oneOf": [ + { + "description": "Enables the fetch command without any pre-configured scope.", + "type": "string", + "const": "allow-fetch", + "markdownDescription": "Enables the fetch command without any pre-configured scope." + }, + { + "description": "Denies the fetch command without any pre-configured scope.", + "type": "string", + "const": "deny-fetch", + "markdownDescription": "Denies the fetch command without any pre-configured scope." + }, + { + "description": "Enables the fetch_cancel command without any pre-configured scope.", + "type": "string", + "const": "allow-fetch-cancel", + "markdownDescription": "Enables the fetch_cancel command without any pre-configured scope." + }, + { + "description": "Denies the fetch_cancel command without any pre-configured scope.", + "type": "string", + "const": "deny-fetch-cancel", + "markdownDescription": "Denies the fetch_cancel command without any pre-configured scope." + }, + { + "description": "Enables the fetch_cancel_body command without any pre-configured scope.", + "type": "string", + "const": "allow-fetch-cancel-body", + "markdownDescription": "Enables the fetch_cancel_body command without any pre-configured scope." + }, + { + "description": "Denies the fetch_cancel_body command without any pre-configured scope.", + "type": "string", + "const": "deny-fetch-cancel-body", + "markdownDescription": "Denies the fetch_cancel_body command without any pre-configured scope." + }, + { + "description": "Enables the fetch_read_body command without any pre-configured scope.", + "type": "string", + "const": "allow-fetch-read-body", + "markdownDescription": "Enables the fetch_read_body command without any pre-configured scope." + }, + { + "description": "Denies the fetch_read_body command without any pre-configured scope.", + "type": "string", + "const": "deny-fetch-read-body", + "markdownDescription": "Denies the fetch_read_body command without any pre-configured scope." + }, + { + "description": "Enables the fetch_send command without any pre-configured scope.", + "type": "string", + "const": "allow-fetch-send", + "markdownDescription": "Enables the fetch_send command without any pre-configured scope." + }, + { + "description": "Denies the fetch_send command without any pre-configured scope.", + "type": "string", + "const": "deny-fetch-send", + "markdownDescription": "Denies the fetch_send command without any pre-configured scope." + }, + { + "description": "This permission set configures what kind of\nfetch operations are available from the http plugin.\n\nThis enables all fetch operations but does not\nallow explicitly any origins to be fetched. This needs to\nbe manually configured before usage.\n\n#### Granted Permissions\n\nAll fetch operations are enabled.\n\n\n#### This default permission set includes:\n\n- `allow-fetch`\n- `allow-fetch-cancel`\n- `allow-fetch-send`\n- `allow-fetch-read-body`\n- `allow-fetch-cancel-body`", + "type": "string", + "const": "default", + "markdownDescription": "This permission set configures what kind of\nfetch operations are available from the http plugin.\n\nThis enables all fetch operations but does not\nallow explicitly any origins to be fetched. This needs to\nbe manually configured before usage.\n\n#### Granted Permissions\n\nAll fetch operations are enabled.\n\n\n#### This default permission set includes:\n\n- `allow-fetch`\n- `allow-fetch-cancel`\n- `allow-fetch-send`\n- `allow-fetch-read-body`\n- `allow-fetch-cancel-body`" + } + ] + } + } +} \ No newline at end of file diff --git a/plugins/http/rollup.config.js b/plugins/http/rollup.config.js new file mode 100644 index 0000000..1f349ec --- /dev/null +++ b/plugins/http/rollup.config.js @@ -0,0 +1,7 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +import { createConfig } from '../../shared/rollup.config.js' + +export default createConfig() diff --git a/plugins/http/src/commands.rs b/plugins/http/src/commands.rs new file mode 100644 index 0000000..4387bae --- /dev/null +++ b/plugins/http/src/commands.rs @@ -0,0 +1,718 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use std::{future::Future, pin::Pin, str::FromStr, sync::Arc, time::Duration}; + +use http::{header, HeaderMap, HeaderName, HeaderValue, Method, StatusCode}; +use reqwest::{redirect::Policy, NoProxy}; +use serde::{Deserialize, Serialize}; +use tauri::{ + async_runtime::Mutex, + command, + ipc::{CommandScope, GlobalScope}, + Manager, ResourceId, ResourceTable, Runtime, State, Webview, +}; +use tokio::sync::oneshot::{channel, Receiver, Sender}; + +use crate::{ + scope::{Entry, Scope}, + Error, Http, Result, +}; + +const HTTP_USER_AGENT: &str = concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION"),); + +/// Matches the default [`reqwest`] redirect policy, used when the frontend does not +/// configure `maxRedirections`. +const DEFAULT_MAX_REDIRECTIONS: usize = 10; + +struct ReqwestResponse(reqwest::Response); +impl tauri::Resource for ReqwestResponse {} + +type CancelableResponseResult = Result; +type CancelableResponseFuture = + Pin + Send + Sync>>; + +struct FetchRequest { + fut: Mutex, + abort_tx_rid: ResourceId, + abort_rx_rid: ResourceId, +} +impl tauri::Resource for FetchRequest {} + +struct AbortSender(Sender<()>); +impl tauri::Resource for AbortRecveiver {} + +impl AbortSender { + fn abort(self) { + let _ = self.0.send(()); + } +} + +struct AbortRecveiver(Receiver<()>); +impl tauri::Resource for AbortSender {} + +trait AddRequest { + fn add_request(&mut self, fut: CancelableResponseFuture) -> ResourceId; +} + +impl AddRequest for ResourceTable { + fn add_request(&mut self, fut: CancelableResponseFuture) -> ResourceId { + let (tx, rx) = channel::<()>(); + let (tx, rx) = (AbortSender(tx), AbortRecveiver(rx)); + let req = FetchRequest { + fut: Mutex::new(fut), + abort_tx_rid: self.add(tx), + abort_rx_rid: self.add(rx), + }; + self.add(req) + } +} + +#[derive(Serialize)] +#[serde(rename_all = "camelCase")] +pub struct FetchResponse { + status: u16, + status_text: String, + headers: Vec<(String, String)>, + url: String, + rid: ResourceId, +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +#[allow(dead_code)] //feature flags shoudln't affect api +pub struct DangerousSettings { + accept_invalid_certs: bool, + accept_invalid_hostnames: bool, +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ClientConfig { + method: String, + url: url::Url, + headers: Vec<(String, String)>, + data: Option>, + connect_timeout: Option, + max_redirections: Option, + proxy: Option, + danger: Option, +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct Proxy { + all: Option, + http: Option, + https: Option, +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +#[serde(untagged)] +pub enum UrlOrConfig { + Url(String), + Config(ProxyConfig), +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ProxyConfig { + url: String, + basic_auth: Option, + no_proxy: Option, +} + +#[derive(Debug, Deserialize)] +pub struct BasicAuth { + username: String, + password: String, +} + +#[inline] +fn proxy_creator( + url_or_config: UrlOrConfig, + proxy_fn: fn(String) -> reqwest::Result, +) -> reqwest::Result { + match url_or_config { + UrlOrConfig::Url(url) => Ok(proxy_fn(url)?), + UrlOrConfig::Config(ProxyConfig { + url, + basic_auth, + no_proxy, + }) => { + let mut proxy = proxy_fn(url)?; + if let Some(basic_auth) = basic_auth { + proxy = proxy.basic_auth(&basic_auth.username, &basic_auth.password); + } + if let Some(no_proxy) = no_proxy { + proxy = proxy.no_proxy(NoProxy::from_string(&no_proxy)); + } + Ok(proxy) + } + } +} + +fn attach_proxy( + proxy: Proxy, + mut builder: reqwest::ClientBuilder, +) -> crate::Result { + let Proxy { all, http, https } = proxy; + + if let Some(all) = all { + let proxy = proxy_creator(all, reqwest::Proxy::all)?; + builder = builder.proxy(proxy); + } + + if let Some(http) = http { + let proxy = proxy_creator(http, reqwest::Proxy::http)?; + builder = builder.proxy(proxy); + } + + if let Some(https) = https { + let proxy = proxy_creator(https, reqwest::Proxy::https)?; + builder = builder.proxy(proxy); + } + + Ok(builder) +} + +/// Builds the redirect policy for the request. +/// +/// When `scope` is [`Some`], **every** hop is checked against it. Validating only the URL the +/// frontend asked for is not enough: a server on an allowed origin can answer with a redirect to +/// any other origin (an open redirect, or a server the attacker controls), and following it would +/// give the webview access to a URL the scope denies. +/// +/// That check is opt-in through the `scopeRedirects` configuration because it breaks applications +/// that rely on being redirected outside of their scope. +// TODO(v3): always check the scope and take it by value instead of `Option` +fn redirect_policy(scope: Option, max_redirections: Option) -> Policy { + if max_redirections == Some(0) { + return Policy::none(); + } + + let max_redirections = max_redirections.unwrap_or(DEFAULT_MAX_REDIRECTIONS); + + let Some(scope) = scope else { + return Policy::limited(max_redirections); + }; + + Policy::custom(move |attempt| { + // the first URL in `previous` is the initial request, so it must be excluded + if attempt.previous().len() > max_redirections { + attempt.error("too many redirects") + } else if scope.is_allowed(attempt.url()) { + attempt.follow() + } else { + let url = attempt.url().clone(); + attempt.error(Error::UrlNotAllowed(url)) + } + }) +} + +/// [`reqwest`] wraps the error returned by the redirect policy, and its `Display` impl does not +/// include the source, so we unwrap our own scope error to keep the reason visible to the frontend. +fn map_request_error(error: reqwest::Error) -> Error { + if error.is_redirect() { + let mut source = std::error::Error::source(&error); + while let Some(err) = source { + if let Some(Error::UrlNotAllowed(url)) = err.downcast_ref::() { + return Error::UrlNotAllowed(url.clone()); + } + source = err.source(); + } + } + + Error::Network(error) +} + +#[command] +pub async fn fetch( + webview: Webview, + state: State<'_, Http>, + client_config: ClientConfig, + command_scope: CommandScope, + global_scope: GlobalScope, +) -> crate::Result { + let ClientConfig { + method, + url, + headers: headers_raw, + data, + connect_timeout, + max_redirections, + proxy, + danger, + } = client_config; + + let scheme = url.scheme(); + let method = Method::from_bytes(method.as_bytes())?; + + let mut headers = HeaderMap::new(); + for (h, v) in headers_raw { + let name = HeaderName::from_str(&h)?; + #[cfg(not(feature = "unsafe-headers"))] + if is_unsafe_header(&name) { + #[cfg(debug_assertions)] + { + eprintln!("[\x1b[33mWARNING\x1b[0m] Skipping {name} header as it is a forbidden header per fetch spec https://fetch.spec.whatwg.org/#terminology-headers"); + eprintln!("[\x1b[33mWARNING\x1b[0m] if keeping the header is a desired behavior, you can enable `unsafe-headers` feature flag in your Cargo.toml"); + } + continue; + } + + headers.append(name, HeaderValue::from_str(&v)?); + } + + match scheme { + "http" | "https" => { + let scope = Scope::new( + command_scope + .allows() + .iter() + .chain(global_scope.allows()) + .cloned() + .collect(), + command_scope + .denies() + .iter() + .chain(global_scope.denies()) + .cloned() + .collect(), + ); + + if !scope.is_allowed(&url) { + return Err(Error::UrlNotAllowed(url)); + } + + let mut builder = reqwest::ClientBuilder::new(); + + if let Some(danger_config) = danger { + #[cfg(not(feature = "dangerous-settings"))] + { + #[cfg(debug_assertions)] + { + eprintln!("[\x1b[33mWARNING\x1b[0m] using dangerous settings requires `dangerous-settings` feature flag in your Cargo.toml"); + } + let _ = danger_config; + return Err(Error::DangerousSettings); + } + #[cfg(feature = "dangerous-settings")] + { + builder = builder + .danger_accept_invalid_certs(danger_config.accept_invalid_certs) + .danger_accept_invalid_hostnames(danger_config.accept_invalid_hostnames) + } + } + + if let Some(timeout) = connect_timeout { + builder = builder.connect_timeout(Duration::from_millis(timeout)); + } + + let scope = state.config.scope_redirects.then_some(scope); + builder = builder.redirect(redirect_policy(scope, max_redirections)); + + if let Some(proxy_config) = proxy { + builder = attach_proxy(proxy_config, builder)?; + } + + #[cfg(feature = "cookies")] + { + builder = builder.cookie_provider(state.cookies_jar.clone()); + } + + let mut request = builder.build()?.request(method.clone(), url); + + // POST and PUT requests should always have a 0 length content-length, + // if there is no body. https://fetch.spec.whatwg.org/#http-network-or-cache-fetch + if data.is_none() && matches!(method, Method::POST | Method::PUT) { + headers.append(header::CONTENT_LENGTH, HeaderValue::from_str("0")?); + } + + if headers.contains_key(header::RANGE) { + // https://fetch.spec.whatwg.org/#http-network-or-cache-fetch step 18 + // If httpRequest's header list contains `Range`, then append (`Accept-Encoding`, `identity`) + headers.append(header::ACCEPT_ENCODING, HeaderValue::from_str("identity")?); + } + + if !headers.contains_key(header::USER_AGENT) { + headers.append(header::USER_AGENT, HeaderValue::from_str(HTTP_USER_AGENT)?); + } + + // ensure we have an Origin header set + if cfg!(not(feature = "unsafe-headers")) || !headers.contains_key(header::ORIGIN) { + if let Ok(url) = webview.url() { + // The url crate returns OpaqueOrigin for tauri://localhost which serializes to "null" + let origin = if url.scheme() == "tauri" { + "tauri://localhost".to_string() + } else { + url.origin().ascii_serialization() + }; + headers.append(header::ORIGIN, HeaderValue::from_str(&origin)?); + } + } + + // In case empty origin is passed, remove it. Some services do not like Origin header + // so this way we can remove it in explicit way. The default behaviour is still to set it + if cfg!(feature = "unsafe-headers") + && headers.get(header::ORIGIN) == Some(&HeaderValue::from_static("")) + { + headers.remove(header::ORIGIN); + }; + + if let Some(data) = data { + request = request.body(data); + } + + request = request.headers(headers); + + #[cfg(feature = "tracing")] + tracing::trace!("{:?}", request); + + let fut = async move { request.send().await.map_err(map_request_error) }; + + let mut resources_table = webview.resources_table(); + let rid = resources_table.add_request(Box::pin(fut)); + + Ok(rid) + } + "data" => { + let data_url = + data_url::DataUrl::process(url.as_str()).map_err(|_| Error::DataUrlError)?; + let (body, _) = data_url + .decode_to_vec() + .map_err(|_| Error::DataUrlDecodeError)?; + + let response = http::Response::builder() + .status(StatusCode::OK) + .header(header::CONTENT_TYPE, data_url.mime_type().to_string()) + .body(reqwest::Body::from(body))?; + + #[cfg(feature = "tracing")] + tracing::trace!("{:?}", response); + + let fut = async move { Ok(reqwest::Response::from(response)) }; + let mut resources_table = webview.resources_table(); + let rid = resources_table.add_request(Box::pin(fut)); + Ok(rid) + } + _ => Err(Error::SchemeNotSupport(scheme.to_string())), + } +} + +#[command] +pub fn fetch_cancel(webview: Webview, rid: ResourceId) -> crate::Result<()> { + let mut resources_table = webview.resources_table(); + let req = resources_table.get::(rid)?; + let abort_tx = resources_table.take::(req.abort_tx_rid)?; + if let Some(abort_tx) = Arc::into_inner(abort_tx) { + abort_tx.abort(); + } + Ok(()) +} + +#[command] +pub async fn fetch_send( + webview: Webview, + rid: ResourceId, +) -> crate::Result { + let (req, abort_rx) = { + let mut resources_table = webview.resources_table(); + let req = resources_table.get::(rid)?; + let abort_rx = resources_table.take::(req.abort_rx_rid)?; + (req, abort_rx) + }; + + let Some(abort_rx) = Arc::into_inner(abort_rx) else { + return Err(Error::RequestCanceled); + }; + + let mut fut = req.fut.lock().await; + + let res = tokio::select! { + res = fut.as_mut() => res?, + _ = abort_rx.0 => { + let mut resources_table = webview.resources_table(); + resources_table.close(rid)?; + return Err(Error::RequestCanceled); + } + }; + + #[cfg(feature = "tracing")] + tracing::trace!("{:?}", res); + + let status = res.status(); + let url = res.url().to_string(); + let mut headers = Vec::new(); + for (key, val) in res.headers().iter() { + headers.push(( + key.as_str().into(), + String::from_utf8(val.as_bytes().to_vec())?, + )); + } + + let mut resources_table = webview.resources_table(); + let rid = resources_table.add(ReqwestResponse(res)); + + Ok(FetchResponse { + status: status.as_u16(), + status_text: status.canonical_reason().unwrap_or_default().to_string(), + headers, + url, + rid, + }) +} + +#[command] +pub async fn fetch_read_body( + webview: Webview, + rid: ResourceId, +) -> crate::Result { + let res = { + let resources_table = webview.resources_table(); + resources_table.get::(rid)? + }; + + // SAFETY: we can access the inner value mutably + // because we are the only ones with a reference to it + // and we don't want to use `Arc::into_inner` because we want to keep the value in the table + // for potential future calls to `fetch_cancel_body` + let res_ptr = Arc::as_ptr(&res) as *mut ReqwestResponse; + let res = unsafe { &mut *res_ptr }; + let res = &mut res.0; + + let Some(chunk) = res.chunk().await? else { + let mut resources_table = webview.resources_table(); + resources_table.close(rid)?; + + // return a response with a single byte to indicate that the body is empty + return Ok(tauri::ipc::Response::new(vec![1])); + }; + + let mut chunk = chunk.to_vec(); + // append a 0 byte to indicate that the body is not empty + chunk.push(0); + + Ok(tauri::ipc::Response::new(chunk)) +} + +#[command] +pub async fn fetch_cancel_body( + webview: Webview, + rid: ResourceId, +) -> crate::Result<()> { + let mut resources_table = webview.resources_table(); + resources_table.close(rid)?; + Ok(()) +} + +// forbidden headers per fetch spec https://fetch.spec.whatwg.org/#terminology-headers +#[cfg(not(feature = "unsafe-headers"))] +fn is_unsafe_header(header: &HeaderName) -> bool { + matches!( + *header, + header::ACCEPT_CHARSET + | header::ACCEPT_ENCODING + | header::ACCESS_CONTROL_REQUEST_HEADERS + | header::ACCESS_CONTROL_REQUEST_METHOD + | header::CONNECTION + | header::CONTENT_LENGTH + | header::COOKIE + | header::DATE + | header::DNT + | header::EXPECT + | header::HOST + | header::ORIGIN + | header::REFERER + | header::SET_COOKIE + | header::TE + | header::TRAILER + | header::TRANSFER_ENCODING + | header::UPGRADE + | header::VIA + ) || { + let lower = header.as_str().to_lowercase(); + lower.starts_with("proxy-") || lower.starts_with("sec-") + } +} + +#[cfg(test)] +mod tests { + use std::{ + io::{BufRead, BufReader, Write}, + net::TcpListener, + sync::Arc, + }; + + use super::*; + + /// Test server with an open redirect: + /// + /// - `/redirect-external` answers a 302 to `http://127.0.0.1:{port}/secret`, which is a + /// different origin as far as the scope is concerned - the tests allow `localhost`; + /// - `/redirect/{n}` answers a 302 to `/redirect/{n-1}` on `localhost`, and `/redirect/0` + /// answers a 302 to `/target`, so the whole chain stays in scope; + /// - anything else answers a 200, so a bypass shows up as a successful response. + fn spawn_server() -> u16 { + let listener = TcpListener::bind("127.0.0.1:0").unwrap(); + let port = listener.local_addr().unwrap().port(); + + std::thread::spawn(move || { + for stream in listener.incoming() { + let mut stream = stream.unwrap(); + + // the whole request must be consumed, otherwise closing the connection + // with unread data queued resets it before the client reads the response + let mut reader = BufReader::new(stream.try_clone().unwrap()); + let mut request_line = String::new(); + reader.read_line(&mut request_line).unwrap(); + loop { + let mut header = String::new(); + reader.read_line(&mut header).unwrap(); + if matches!(header.as_str(), "" | "\r\n" | "\n") { + break; + } + } + + let path = request_line.split(' ').nth(1).unwrap_or("/"); + let location = match path.strip_prefix("/redirect/") { + Some("0") => Some(format!("http://localhost:{port}/target")), + Some(remaining) => Some(format!( + "http://localhost:{port}/redirect/{}", + remaining.parse::().unwrap() - 1 + )), + None if path == "/redirect-external" => { + Some(format!("http://127.0.0.1:{port}/secret")) + } + None => None, + }; + + let response = match location { + Some(location) => format!( + "HTTP/1.1 302 Found\r\nLocation: {location}\r\nContent-Length: 0\r\nConnection: close\r\n\r\n" + ), + None => "HTTP/1.1 200 OK\r\nContent-Length: 6\r\nConnection: close\r\n\r\nsecret" + .to_string(), + }; + + let _ = stream.write_all(response.as_bytes()); + let _ = stream.flush(); + } + }); + + port + } + + fn get( + url: &str, + scope: Option, + max_redirections: Option, + ) -> Result { + let client = reqwest::ClientBuilder::new() + .redirect(redirect_policy(scope, max_redirections)) + .build() + .unwrap(); + let request = client.get(url); + tauri::async_runtime::block_on( + async move { request.send().await.map_err(map_request_error) }, + ) + } + + fn localhost_scope(port: u16) -> Option { + let entry = Arc::new(format!("http://localhost:{port}/*").parse().unwrap()); + Some(Scope::new(vec![entry], Vec::new())) + } + + #[test] + fn redirect_outside_of_scope_is_denied() { + let port = spawn_server(); + + let err = get( + &format!("http://localhost:{port}/redirect-external"), + localhost_scope(port), + None, + ) + .unwrap_err(); + + match err { + Error::UrlNotAllowed(url) => { + assert_eq!(url.as_str(), format!("http://127.0.0.1:{port}/secret")) + } + e => panic!("expected the redirect to be denied by the scope, got {e:?}"), + } + } + + #[test] + fn redirect_outside_of_scope_is_followed_when_not_configured() { + let port = spawn_server(); + + // `scopeRedirects` is disabled, so only the URL requested by the frontend is checked + let response = get( + &format!("http://localhost:{port}/redirect-external"), + None, + None, + ) + .unwrap(); + + assert_eq!(response.status(), StatusCode::OK); + } + + #[test] + fn redirect_denied_by_the_scope_is_denied() { + let port = spawn_server(); + let allow = Arc::new(format!("http://localhost:{port}/*").parse().unwrap()); + let deny = Arc::new(format!("http://localhost:{port}/target").parse().unwrap()); + let scope = Some(Scope::new(vec![allow], vec![deny])); + + let err = get(&format!("http://localhost:{port}/redirect/0"), scope, None).unwrap_err(); + + assert!(matches!(err, Error::UrlNotAllowed(_))); + } + + #[test] + fn redirect_inside_of_scope_is_followed() { + let port = spawn_server(); + + let response = get( + &format!("http://localhost:{port}/redirect/2"), + localhost_scope(port), + None, + ) + .unwrap(); + + assert_eq!(response.status(), StatusCode::OK); + assert_eq!(response.url().path(), "/target"); + } + + #[test] + fn max_redirections_is_enforced() { + let port = spawn_server(); + + for scope in [localhost_scope(port), None] { + let err = get( + &format!("http://localhost:{port}/redirect/5"), + scope, + Some(2), + ) + .unwrap_err(); + + assert!(matches!(err, Error::Network(e) if e.is_redirect())); + } + } + + #[test] + fn zero_max_redirections_does_not_follow() { + let port = spawn_server(); + + let response = get( + &format!("http://localhost:{port}/redirect/0"), + localhost_scope(port), + Some(0), + ) + .unwrap(); + + assert_eq!(response.status(), StatusCode::FOUND); + } +} diff --git a/plugins/http/src/config.rs b/plugins/http/src/config.rs new file mode 100644 index 0000000..02f4e92 --- /dev/null +++ b/plugins/http/src/config.rs @@ -0,0 +1,47 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use serde::Deserialize; + +/// HTTP plugin configuration, defined on the `plugins > http` object of your `tauri.conf.json`. +#[derive(Debug, Clone, Default, Deserialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct Config { + /// Whether the scope is checked on every hop of a redirect chain instead of only on the + /// URL requested by the frontend. Defaults to `false`. + /// + /// When disabled, a server on an allowed origin can answer with a redirect to any other + /// origin - an open redirect, or a server an attacker controls - and the plugin follows it, + /// handing the webview a response from a URL the scope denies, such as a `localhost` + /// service, an internal host or a cloud metadata endpoint. + /// + /// When enabled, a redirect to a URL that is not allowed by the scope fails with + /// [`Error::UrlNotAllowed`](crate::Error::UrlNotAllowed) instead of being followed, so every + /// redirect target must also be part of the scope. This is opt-in because it breaks + /// applications that rely on being redirected outside of their configured scope. + // TODO(v3): enforce the scope on redirects by default and remove this option + #[serde(default)] + pub scope_redirects: bool, +} + +#[cfg(test)] +mod tests { + use super::Config; + + #[test] + fn deserializes_the_plugin_configuration() { + // the plugin configuration is optional, and the scope check is opt-in + let config: Option = serde_json::from_value(serde_json::Value::Null).unwrap(); + assert!(config.is_none()); + + let config: Config = serde_json::from_str("{}").unwrap(); + assert!(!config.scope_redirects); + + let config: Config = serde_json::from_str(r#"{ "scopeRedirects": true }"#).unwrap(); + assert!(config.scope_redirects); + + // a typo must not silently disable the scope check + assert!(serde_json::from_str::(r#"{ "scopeRedirect": true }"#).is_err()); + } +} diff --git a/plugins/http/src/error.rs b/plugins/http/src/error.rs new file mode 100644 index 0000000..2b974e6 --- /dev/null +++ b/plugins/http/src/error.rs @@ -0,0 +1,84 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use serde::{Serialize, Serializer}; +use url::Url; + +/// Errors that can happen while using the HTTP plugin. +/// +/// The error is serialized to the frontend as its [`Display`](std::fmt::Display) string. +#[derive(Debug, thiserror::Error)] +pub enum Error { + /// JSON serialization or deserialization error. + #[error(transparent)] + Json(#[from] serde_json::Error), + /// File system error, raised for instance when the cookie store file cannot be created or + /// opened on startup. + #[error(transparent)] + Io(#[from] std::io::Error), + /// Error from the underlying [`reqwest`] client, raised while building the client, sending the + /// request or reading the response body. + #[error(transparent)] + Network(#[from] reqwest::Error), + /// Error from the [`http`] crate, raised while building the response of a `data:` URL. + #[error(transparent)] + Http(#[from] http::Error), + /// A header name given by the frontend is not a valid HTTP header name. + #[error(transparent)] + HttpInvalidHeaderName(#[from] http::header::InvalidHeaderName), + /// A header value is not a valid HTTP header value. + #[error(transparent)] + HttpInvalidHeaderValue(#[from] http::header::InvalidHeaderValue), + /// URL not allowed by the scope. + /// + /// Raised for the URL requested by the frontend, and - when the + /// [`scope_redirects`](crate::Config::scope_redirects) configuration is enabled - for any + /// redirect target that is not allowed by the scope. + #[error("url not allowed on the configured scope: {0}")] + UrlNotAllowed(Url), + /// Failed to parse a URL. + #[error(transparent)] + UrlParseError(#[from] url::ParseError), + /// HTTP method error. + #[error(transparent)] + HttpMethod(#[from] http::method::InvalidMethod), + /// The requested URL uses an unsupported scheme. Only `http`, `https` and `data` are handled. + #[error("scheme {0} not supported")] + SchemeNotSupport(String), + /// The request was aborted by the frontend before the response was received. + #[error("Request canceled")] + RequestCanceled, + /// Error from the file system plugin. + #[error(transparent)] + FsError(#[from] tauri_plugin_fs::Error), + /// The `data:` URL could not be processed. + #[error("failed to process data url")] + DataUrlError, + /// The body of the `data:` URL could not be decoded into bytes. + #[error("failed to decode data url into bytes")] + DataUrlDecodeError, + /// Error from the Tauri APIs, raised for instance while resolving a path or while reading the + /// webview resource table. + #[error(transparent)] + Tauri(#[from] tauri::Error), + /// A response header value is not valid UTF-8, so it cannot be forwarded to the frontend. + #[error(transparent)] + Utf8(#[from] std::string::FromUtf8Error), + /// The frontend requested dangerous client settings, but the `dangerous-settings` Cargo + /// feature is not enabled. + #[error("dangerous settings used but are not enabled")] + DangerousSettings, +} + +impl Serialize for Error { + fn serialize(&self, serializer: S) -> std::result::Result + where + S: Serializer, + { + serializer.serialize_str(self.to_string().as_ref()) + } +} + +/// Alias for a [`Result`](std::result::Result) with the error type [`Error`]. +pub type Result = std::result::Result; diff --git a/plugins/http/src/lib.rs b/plugins/http/src/lib.rs new file mode 100644 index 0000000..6444ef9 --- /dev/null +++ b/plugins/http/src/lib.rs @@ -0,0 +1,160 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +//! Access the HTTP client written in Rust. +//! +//! ## Cargo features +//! +//! ### Reqwest feature forwards +//! +//! These features forwards [`reqwest`](https://docs.rs/reqwest/0.12.28/reqwest/index.html) features: +//! +//! - **http2** *(enabled by default)*: Enables HTTP/2 support. +//! - **native-tls**: Enables TLS functionality provided by native-tls. +//! - **native-tls-vendored**: Enables the vendored feature of native-tls. +//! - **native-tls-alpn**: Enables the alpn feature of native-tls. +//! - **rustls-tls** *(enabled by default)*: Enables TLS functionality provided by rustls. Equivalent to +//! rustls-tls-webpki-roots. +//! - **rustls-tls-manual-roots**: Enables TLS functionality provided by rustls, without setting any root +//! certificates. Roots have to be specified manually. +//! - **rustls-tls-webpki-roots**: Enables TLS functionality provided by rustls, while using root certificates +//! from the webpki-roots crate. +//! - **rustls-tls-native-roots**: Enables TLS functionality provided by rustls, while using root certificates +//! from the rustls-native-certs crate. +//! - **blocking**: Provides the [blocking](https://docs.rs/reqwest/0.12.28/reqwest/blocking/index.html) client API. +//! - **charset** *(enabled by default)*: Improved support for decoding text. +//! - **cookies** *(enabled by default)*: Provides cookie session support. +//! - **gzip**: Provides response body gzip decompression. +//! - **brotli**: Provides response body brotli decompression. +//! - **zstd**: Provides response body zstd decompression. +//! - **deflate**: Provides response body deflate decompression. +//! - **json**: Provides serialization and deserialization for JSON bodies. +//! - **multipart**: Provides functionality for multipart forms. +//! - **stream**: Adds support for futures::Stream. +//! - **socks**: Provides SOCKS5 proxy support. +//! - **trust-dns**: Enables a trust-dns/Hickory DNS async resolver instead of the default threadpool using +//! getaddrinfo. +//! - **macos-system-configuration** *(deprecated, use `system-proxy` instead)*: Use Windows and macOS system proxy settings automatically. +//! - **system-proxy** *(enabled by default)*: Use Windows and macOS system proxy settings automatically. +//! +//! ### tauri-plugin-http features +//! +//! - **tracing**: Adds request, response, and cookie-store diagnostics through `tracing`. +//! - **unsafe-headers**: Allows webview requests to send any headers. +//! - **dangerous-settings**: Allows dangerous client settings such as accepting invalid certificates or hostnames. +//! +//! ## Configuration +//! +//! See [`Config`] for the options that can be set on the `plugins > http` object of your +//! `tauri.conf.json`: +//! +//! ```json +//! { +//! "plugins": { +//! "http": { +//! "scopeRedirects": true +//! } +//! } +//! } +//! ``` + +pub use reqwest; +use tauri::{ + plugin::{Builder, TauriPlugin}, + Manager, Runtime, +}; + +pub use config::Config; +pub use error::{Error, Result}; + +mod commands; +mod config; +mod error; +#[cfg(feature = "cookies")] +mod reqwest_cookie_store; +mod scope; + +#[cfg(feature = "cookies")] +const COOKIES_FILENAME: &str = ".cookies"; + +pub(crate) struct Http { + pub(crate) config: Config, + #[cfg(feature = "cookies")] + cookies_jar: std::sync::Arc, +} + +/// Initializes the plugin. +/// +/// The plugin reads its [`Config`] from the `plugins > http` object of the `tauri.conf.json` file; +/// when that object is missing, [`Config::default`] is used. +/// +/// With the `cookies` Cargo feature (enabled by default), a cookie jar is loaded from a `.cookies` +/// file in the application cache directory on setup and written back to it when the application +/// exits. A jar that cannot be read is replaced by an empty one. +/// +/// Register it on the Tauri builder with `.plugin(tauri_plugin_http::init())`. +pub fn init() -> TauriPlugin> { + Builder::>::new("http") + .setup(|app, api| { + #[cfg(feature = "cookies")] + let cookies_jar = { + use crate::reqwest_cookie_store::*; + use std::fs::File; + use std::io::BufReader; + + let cache_dir = app.path().app_cache_dir()?; + std::fs::create_dir_all(&cache_dir)?; + + let path = cache_dir.join(COOKIES_FILENAME); + let file = File::options() + .create(true) + .append(true) + .read(true) + .open(&path)?; + + let reader = BufReader::new(file); + CookieStoreMutex::load(path.clone(), reader).unwrap_or_else(|_e| { + #[cfg(feature = "tracing")] + tracing::warn!( + "failed to load cookie store: {_e}, falling back to empty store" + ); + CookieStoreMutex::new(path, Default::default()) + }) + }; + + let state = Http { + config: api.config().clone().unwrap_or_default(), + #[cfg(feature = "cookies")] + cookies_jar: std::sync::Arc::new(cookies_jar), + }; + + app.manage(state); + + Ok(()) + }) + .on_event(|app, event| { + #[cfg(feature = "cookies")] + if let tauri::RunEvent::Exit = event { + let state = app.state::(); + + match state.cookies_jar.request_save() { + Ok(rx) => { + let _ = rx.recv(); + } + Err(_e) => { + #[cfg(feature = "tracing")] + tracing::error!("failed to save cookie jar: {_e}"); + } + } + } + }) + .invoke_handler(tauri::generate_handler![ + commands::fetch, + commands::fetch_cancel, + commands::fetch_send, + commands::fetch_read_body, + commands::fetch_cancel_body, + ]) + .build() +} diff --git a/plugins/http/src/reqwest_cookie_store.rs b/plugins/http/src/reqwest_cookie_store.rs new file mode 100644 index 0000000..0b71902 --- /dev/null +++ b/plugins/http/src/reqwest_cookie_store.rs @@ -0,0 +1,133 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +// taken from https://github.com/pfernie/reqwest_cookie_store/blob/2ec4afabcd55e24d3afe3f0626ee6dc97bed938d/src/lib.rs + +use std::{ + path::PathBuf, + sync::{mpsc::Receiver, Mutex}, +}; + +use cookie_store::{CookieStore, RawCookie, RawCookieParseError}; +use reqwest::header::HeaderValue; + +fn set_cookies( + cookie_store: &mut CookieStore, + cookie_headers: &mut dyn Iterator, + url: &url::Url, +) { + let cookies = cookie_headers.filter_map(|val| { + std::str::from_utf8(val.as_bytes()) + .map_err(RawCookieParseError::from) + .and_then(RawCookie::parse) + .map(|c| c.into_owned()) + .ok() + }); + cookie_store.store_response_cookies(cookies, url); +} + +fn cookies(cookie_store: &CookieStore, url: &url::Url) -> Option { + let s = cookie_store + .get_request_values(url) + .map(|(name, value)| format!("{name}={value}")) + .collect::>() + .join("; "); + + if s.is_empty() { + return None; + } + + HeaderValue::from_maybe_shared(bytes::Bytes::from(s)).ok() +} + +/// A [`cookie_store::CookieStore`] wrapped internally by a [`std::sync::Mutex`], suitable for use in +/// async/concurrent contexts. +#[derive(Debug)] +pub struct CookieStoreMutex { + pub path: PathBuf, + store: Mutex, + save_task: Mutex>, +} + +impl CookieStoreMutex { + /// Create a new [`CookieStoreMutex`] from an existing [`cookie_store::CookieStore`]. + pub fn new(path: PathBuf, cookie_store: CookieStore) -> CookieStoreMutex { + CookieStoreMutex { + path, + store: Mutex::new(cookie_store), + save_task: Default::default(), + } + } + + pub fn load( + path: PathBuf, + reader: R, + ) -> cookie_store::Result { + cookie_store::serde::load(reader, |c| serde_json::from_str(c)) + .map(|store| CookieStoreMutex::new(path, store)) + } + + fn cookies_to_str(&self) -> Result { + let mut cookies = Vec::new(); + for cookie in self + .store + .lock() + .expect("poisoned cookie jar mutex") + .iter_unexpired() + { + if cookie.is_persistent() { + cookies.push(cookie.clone()); + } + } + serde_json::to_string(&cookies) + } + + pub fn request_save(&self) -> cookie_store::Result> { + let cookie_str = self.cookies_to_str()?; + let path = self.path.clone(); + let (tx, rx) = std::sync::mpsc::channel(); + let task = tauri::async_runtime::spawn(async move { + match tokio::fs::write(&path, &cookie_str).await { + Ok(()) => { + let _ = tx.send(()); + } + Err(_e) => { + #[cfg(feature = "tracing")] + tracing::error!("failed to save cookie jar: {_e}"); + } + } + }); + self.save_task + .lock() + .unwrap() + .replace(CancellableTask(task)); + Ok(rx) + } +} + +impl reqwest::cookie::CookieStore for CookieStoreMutex { + fn set_cookies(&self, cookie_headers: &mut dyn Iterator, url: &url::Url) { + set_cookies(&mut self.store.lock().unwrap(), cookie_headers, url); + + // try to persist cookies immediately asynchronously + if let Err(_e) = self.request_save() { + #[cfg(feature = "tracing")] + tracing::error!("failed to save cookie jar: {_e}"); + } + } + + fn cookies(&self, url: &url::Url) -> Option { + let store = self.store.lock().unwrap(); + cookies(&store, url) + } +} + +#[derive(Debug)] +struct CancellableTask(tauri::async_runtime::JoinHandle<()>); + +impl Drop for CancellableTask { + fn drop(&mut self) { + self.0.abort(); + } +} diff --git a/plugins/http/src/scope.rs b/plugins/http/src/scope.rs new file mode 100644 index 0000000..a7745c2 --- /dev/null +++ b/plugins/http/src/scope.rs @@ -0,0 +1,230 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use std::sync::Arc; + +use serde::{Deserialize, Deserializer}; +use url::Url; +use urlpattern::{UrlPattern, UrlPatternMatchInput}; + +#[allow(rustdoc::bare_urls)] +#[derive(Debug)] +pub struct Entry { + pub url: UrlPattern, +} + +fn parse_url_pattern(s: &str) -> Result { + let mut init = urlpattern::UrlPatternInit::parse_constructor_string::(s, None)?; + if init.search.as_ref().map(|p| p.is_empty()).unwrap_or(true) { + init.search.replace("*".to_string()); + } + if init.hash.as_ref().map(|p| p.is_empty()).unwrap_or(true) { + init.hash.replace("*".to_string()); + } + if init + .pathname + .as_ref() + .map(|p| p.is_empty() || p == "/") + .unwrap_or(true) + { + init.pathname.replace("*".to_string()); + } + UrlPattern::parse(init, Default::default()) +} + +#[derive(Deserialize)] +#[serde(untagged)] +pub(crate) enum EntryRaw { + Value(String), + Object { url: String }, +} + +impl<'de> Deserialize<'de> for Entry { + fn deserialize(deserializer: D) -> std::result::Result + where + D: Deserializer<'de>, + { + EntryRaw::deserialize(deserializer).and_then(|raw| { + let url = match raw { + EntryRaw::Value(url) => url, + EntryRaw::Object { url } => url, + }; + Ok(Entry { + url: parse_url_pattern(&url).map_err(|e| { + serde::de::Error::custom(format!("`{url}` is not a valid URL pattern: {e}")) + })?, + }) + }) + } +} + +/// Scope for HTTP access. +#[derive(Debug)] +pub struct Scope { + allowed: Vec>, + denied: Vec>, +} + +impl Scope { + /// Creates a new scope from the scope configuration. + pub(crate) fn new(allowed: Vec>, denied: Vec>) -> Self { + Self { allowed, denied } + } + + /// Determines if the given URL is allowed on this scope. + pub fn is_allowed(&self, url: &Url) -> bool { + let denied = self.denied.iter().any(|entry| { + entry + .url + .test(UrlPatternMatchInput::Url(url.clone())) + .unwrap_or_default() + }); + if denied { + false + } else { + self.allowed.iter().any(|entry| { + entry + .url + .test(UrlPatternMatchInput::Url(url.clone())) + .unwrap_or_default() + }) + } + } +} + +#[cfg(test)] +impl std::str::FromStr for Entry { + type Err = urlpattern::quirks::Error; + + fn from_str(s: &str) -> Result { + let url = parse_url_pattern(s)?; + Ok(Self { url }) + } +} + +#[cfg(test)] +mod tests { + use std::sync::Arc; + + #[test] + fn denied_takes_precedence() { + let allow = Arc::new("http://localhost:8080/file.png".parse().unwrap()); + let deny = Arc::new("http://localhost:8080/*".parse().unwrap()); + let scope = super::Scope::new(vec![allow], vec![deny]); + assert!(!scope.is_allowed(&"http://localhost:8080/file.png".parse().unwrap())); + assert!(!scope.is_allowed(&"http://localhost:8080?framework=tauri".parse().unwrap())); + } + + #[test] + fn fixed_url() { + // plain URL + let entry = Arc::new("http://localhost:8080".parse().unwrap()); + let scope = super::Scope::new(vec![entry], Vec::new()); + assert!(scope.is_allowed(&"http://localhost:8080".parse().unwrap())); + assert!(scope.is_allowed(&"http://localhost:8080/".parse().unwrap())); + assert!(scope.is_allowed(&"http://localhost:8080/file".parse().unwrap())); + assert!(scope.is_allowed(&"http://localhost:8080/path/to/asset.png".parse().unwrap())); + assert!(scope.is_allowed(&"http://localhost:8080/path/list?limit=50".parse().unwrap())); + + assert!(!scope.is_allowed(&"https://localhost:8080".parse().unwrap())); + assert!(!scope.is_allowed(&"http://localhost:8081".parse().unwrap())); + assert!(!scope.is_allowed(&"http://local:8080".parse().unwrap())); + } + + #[test] + fn fixed_path() { + // URL with fixed path + let entry = Arc::new("http://localhost:8080/file.png".parse().unwrap()); + let scope = super::Scope::new(vec![entry], Vec::new()); + + assert!(scope.is_allowed(&"http://localhost:8080/file.png".parse().unwrap())); + assert!(scope.is_allowed(&"http://localhost:8080/file.png?q=1".parse().unwrap())); + + assert!(!scope.is_allowed(&"http://localhost:8080".parse().unwrap())); + assert!(!scope.is_allowed(&"http://localhost:8080/file".parse().unwrap())); + assert!(!scope.is_allowed(&"http://localhost:8080/file.png/other.jpg".parse().unwrap())); + } + + #[test] + fn pattern_wildcard() { + let entry = Arc::new("http://localhost:8080/*.png".parse().unwrap()); + let scope = super::Scope::new(vec![entry], Vec::new()); + + assert!(scope.is_allowed(&"http://localhost:8080/file.png".parse().unwrap())); + assert!(scope.is_allowed(&"http://localhost:8080/file.png#head".parse().unwrap())); + assert!(scope.is_allowed(&"http://localhost:8080/assets/file.png".parse().unwrap())); + assert!(scope.is_allowed( + &"http://localhost:8080/assets/file.png?width=100&height=200" + .parse() + .unwrap() + )); + + assert!(!scope.is_allowed(&"http://localhost:8080/file.jpeg".parse().unwrap())); + } + + #[test] + fn domain_wildcard() { + let entry = Arc::new("http://*".parse().unwrap()); + let scope = super::Scope::new(vec![entry], Vec::new()); + + assert!(scope.is_allowed(&"http://something.else".parse().unwrap())); + assert!(scope.is_allowed(&"http://something.else#tauri".parse().unwrap())); + assert!(scope.is_allowed(&"http://something.else/path/to/file".parse().unwrap())); + assert!(scope.is_allowed(&"http://something.else?rel=tauri".parse().unwrap())); + assert!(scope.is_allowed( + &"http://something.else/path/to/file.mp4?start=500" + .parse() + .unwrap() + )); + + assert!(!scope.is_allowed(&"https://something.else".parse().unwrap())); + + let entry = Arc::new("http://*/*".parse().unwrap()); + let scope = super::Scope::new(vec![entry], Vec::new()); + + assert!(scope.is_allowed(&"http://something.else".parse().unwrap())); + assert!(scope.is_allowed(&"http://something.else/path/to/file".parse().unwrap())); + } + + #[test] + fn scheme_wildcard() { + let entry = Arc::new("*://*".parse().unwrap()); + let scope = super::Scope::new(vec![entry], Vec::new()); + + assert!(scope.is_allowed(&"http://something.else".parse().unwrap())); + assert!(scope.is_allowed(&"http://something.else/path/to/file".parse().unwrap())); + assert!(scope.is_allowed(&"file://path".parse().unwrap())); + assert!(scope.is_allowed(&"file://path/to/file".parse().unwrap())); + assert!(scope.is_allowed(&"https://something.else".parse().unwrap())); + assert!(scope.is_allowed(&"https://something.else?x=1#frag".parse().unwrap())); + + let entry = Arc::new("*://*/*".parse().unwrap()); + let scope = super::Scope::new(vec![entry], Vec::new()); + + assert!(scope.is_allowed(&"http://something.else".parse().unwrap())); + assert!(scope.is_allowed(&"http://something.else/path/to/file".parse().unwrap())); + assert!(scope.is_allowed(&"file://path/to/file".parse().unwrap())); + assert!(scope.is_allowed(&"https://something.else".parse().unwrap())); + } + + #[test] + fn validate_query() { + let entry = Arc::new("https://tauri.app/path?x=*".parse().unwrap()); + let scope = super::Scope::new(vec![entry], Vec::new()); + + assert!(scope.is_allowed(&"https://tauri.app/path?x=5".parse().unwrap())); + + assert!(!scope.is_allowed(&"https://tauri.app/path?y=5".parse().unwrap())); + } + + #[test] + fn validate_hash() { + let entry = Arc::new("https://tauri.app/path#frame*".parse().unwrap()); + let scope = super::Scope::new(vec![entry], Vec::new()); + + assert!(scope.is_allowed(&"https://tauri.app/path#frame".parse().unwrap())); + + assert!(!scope.is_allowed(&"https://tauri.app/path#work".parse().unwrap())); + } +} diff --git a/plugins/http/tsconfig.json b/plugins/http/tsconfig.json new file mode 100644 index 0000000..4374468 --- /dev/null +++ b/plugins/http/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "rootDir": "guest-js" + }, + "include": ["guest-js/*.ts"] +} diff --git a/plugins/localhost/CHANGELOG.md b/plugins/localhost/CHANGELOG.md new file mode 100644 index 0000000..c9e7c64 --- /dev/null +++ b/plugins/localhost/CHANGELOG.md @@ -0,0 +1,113 @@ +# Changelog + +## \[2.3.2] + +- [`8bfa4450`](https://github.com/tauri-apps/plugins-workspace/commit/8bfa4450230d6a00f836bd27944c34cd7fe43e08) ([#3112](https://github.com/tauri-apps/plugins-workspace/pull/3112) by [@lucasfernog](https://github.com/tauri-apps/plugins-workspace/../../lucasfernog)) Disable caching on responses. + +## \[2.3.1] + +- [`93426f85`](https://github.com/tauri-apps/plugins-workspace/commit/93426f85120f49beb9f40222bff45185a32d54a9) Fixed an issue that caused docs.rs builds to fail. No user facing changes. + +## \[2.3.0] + +- [`f209b2f2`](https://github.com/tauri-apps/plugins-workspace/commit/f209b2f23cb29133c97ad5961fb46ef794dbe063) ([#2804](https://github.com/tauri-apps/plugins-workspace/pull/2804) by [@renovate](https://github.com/tauri-apps/plugins-workspace/../../renovate)) Updated tauri to 2.6 + +## \[2.2.1] + +- [`f634e524`](https://github.com/tauri-apps/plugins-workspace/commit/f634e5248ebe428f8305a59f74c13fc15147fb8e) This is an "empty" release to update the plugins' source files on crates.io and docs.rs. This should fix docs.rs build failures for projects using tauri plugins as dependencies. + +## \[2.2.0] + +- [`3a79266b`](https://github.com/tauri-apps/plugins-workspace/commit/3a79266b8cf96a55b1ae6339d725567d45a44b1d) ([#2173](https://github.com/tauri-apps/plugins-workspace/pull/2173) by [@FabianLars](https://github.com/tauri-apps/plugins-workspace/../../FabianLars)) Bumped all plugins to `v2.2.0`. From now, the versions for the Rust and JavaScript packages of each plugin will be in sync with each other. + +## \[2.1.0] + +- [`3449dd5a`](https://github.com/tauri-apps/plugins-workspace/commit/3449dd5a8f6d12fee8d6389c034fe47e19d72bcd) ([#1982](https://github.com/tauri-apps/plugins-workspace/pull/1982) by [@arihav](https://github.com/tauri-apps/plugins-workspace/../../arihav)) Add custom host binding to allow external access + +## \[2.0.1] + +- [`a1a82208`](https://github.com/tauri-apps/plugins-workspace/commit/a1a82208ed4ab87f83310be0dc95428aec9ab241) ([#1873](https://github.com/tauri-apps/plugins-workspace/pull/1873) by [@lucasfernog](https://github.com/tauri-apps/plugins-workspace/../../lucasfernog)) Downgrade MSRV to 1.77.2 to support Windows 7. + +## \[2.0.0] + +- [`e2c4dfb6`](https://github.com/tauri-apps/plugins-workspace/commit/e2c4dfb6af43e5dd8d9ceba232c315f5febd55c1) Update to tauri v2 stable release. + +## \[2.0.0-rc.1] + +- [`e2e97db5`](https://github.com/tauri-apps/plugins-workspace/commit/e2e97db51983267f5be84d4f6f0278d58834d1f5) ([#1701](https://github.com/tauri-apps/plugins-workspace/pull/1701) by [@lucasfernog](https://github.com/tauri-apps/plugins-workspace/../../lucasfernog)) Update to tauri 2.0.0-rc.8 + +## \[2.0.0-rc.0] + +- [`9887d1`](https://github.com/tauri-apps/plugins-workspace/commit/9887d14bd0e971c4c0f5c1188fc4005d3fc2e29e) Update to tauri RC. + +## \[2.0.0-beta.9] + +- [`e847cedc`](https://github.com/tauri-apps/plugins-workspace/commit/e847cedc1f46f3e7a2ad81ea579b620bc5b992d7) ([#1402](https://github.com/tauri-apps/plugins-workspace/pull/1402) by [@Legend-Master](https://github.com/tauri-apps/plugins-workspace/../../Legend-Master)) Use no default features on tauri for all plugins so that consumers can use `default-features = false` on tauri, note that this will still enable wry feature on iOS +- [`6de87966`](https://github.com/tauri-apps/plugins-workspace/commit/6de87966ecc00ad9d91c25be452f1f46bd2b7e1f) ([#1597](https://github.com/tauri-apps/plugins-workspace/pull/1597) by [@Legend-Master](https://github.com/tauri-apps/plugins-workspace/../../Legend-Master)) Update to tauri beta.25. + +## \[2.0.0-beta.8] + +- [`22a17980`](https://github.com/tauri-apps/plugins-workspace/commit/22a17980ff4f6f8c40adb1b8f4ffc6dae2fe7e30) ([#1537](https://github.com/tauri-apps/plugins-workspace/pull/1537) by [@lucasfernog](https://github.com/tauri-apps/plugins-workspace/../../lucasfernog)) Update to tauri beta.24. + +## \[2.0.0-beta.7] + +- [`76daee7a`](https://github.com/tauri-apps/plugins-workspace/commit/76daee7aafece34de3092c86e531cf9eb1138989) ([#1512](https://github.com/tauri-apps/plugins-workspace/pull/1512) by [@renovate](https://github.com/tauri-apps/plugins-workspace/../../renovate)) Update to tauri beta.23. + +## \[2.0.0-beta.6] + +- [`9013854f`](https://github.com/tauri-apps/plugins-workspace/commit/9013854f42a49a230b9dbb9d02774765528a923f)([#1382](https://github.com/tauri-apps/plugins-workspace/pull/1382)) Update to tauri beta.22. + +## \[2.0.0-beta.5] + +- [`430bd6f4`](https://github.com/tauri-apps/plugins-workspace/commit/430bd6f4f379bee5d232ae6b098ae131db7f178a)([#1363](https://github.com/tauri-apps/plugins-workspace/pull/1363)) Update to tauri beta.20. + +## \[2.0.0-beta.4] + +- [`bd1ed590`](https://github.com/tauri-apps/plugins-workspace/commit/bd1ed5903ffcce5500310dac1e59e8c67674ef1e)([#1237](https://github.com/tauri-apps/plugins-workspace/pull/1237)) Update to tauri beta.17. + +## \[2.0.0-beta.3] + +- [`a04ea2f`](https://github.com/tauri-apps/plugins-workspace/commit/a04ea2f38294d5a3987578283badc8eec87a7752)([#1071](https://github.com/tauri-apps/plugins-workspace/pull/1071)) The global API script is now only added to the binary when the `withGlobalTauri` config is true. + +## \[2.0.0-beta.2] + +- [`99bea25`](https://github.com/tauri-apps/plugins-workspace/commit/99bea2559c2c0648c2519c50a18cd124dacef57b)([#1005](https://github.com/tauri-apps/plugins-workspace/pull/1005)) Update to tauri beta.8. + +## \[2.0.0-beta.1] + +- [`14f381a`](https://github.com/tauri-apps/plugins-workspace/commit/14f381acf8fe690acecc676922c6f05939b95734) Update MSRV to 1.75. +- [`569defb`](https://github.com/tauri-apps/plugins-workspace/commit/569defbe9492e38938554bb7bdc1be9151456d21) Update to tauri beta.4. + +## \[2.0.0-beta.0] + +- [`d198c01`](https://github.com/tauri-apps/plugins-workspace/commit/d198c014863ee260cb0de88a14b7fc4356ef7474)([#862](https://github.com/tauri-apps/plugins-workspace/pull/862)) Update to tauri beta. +- [`d198c01`](https://github.com/tauri-apps/plugins-workspace/commit/d198c014863ee260cb0de88a14b7fc4356ef7474)([#862](https://github.com/tauri-apps/plugins-workspace/pull/862)) Add permissions. + +## \[2.0.0-alpha.6] + +- [`2cf8faa`](https://github.com/tauri-apps/plugins-workspace/commit/2cf8faa3e149af55eb86e5aba8ebfc54210ca703)([#839](https://github.com/tauri-apps/plugins-workspace/pull/839)) Update to tauri@alpha.20. + +## \[2.0.0-alpha.5] + +- [`387c2f9`](https://github.com/tauri-apps/plugins-workspace/commit/387c2f9e0ce4c75c07ffa3fd76391a25b58f5daf)([#802](https://github.com/tauri-apps/plugins-workspace/pull/802)) Update to tauri@alpha.18. + +## \[2.0.0-alpha.4] + +- [`e438e0a`](https://github.com/tauri-apps/plugins-workspace/commit/e438e0a62d4b430a5159f05f13ecd397dd891a0d)([#676](https://github.com/tauri-apps/plugins-workspace/pull/676)) Update to tauri@alpha.17. + +## \[2.0.0-alpha.3] + +- [`5c13736`](https://github.com/tauri-apps/plugins-workspace/commit/5c137365c60790e8d4037d449e8237aa3fffdab0)([#673](https://github.com/tauri-apps/plugins-workspace/pull/673)) Update MSRV to 1.75. + +## \[2.0.0-alpha.2] + +- [`4e2cef9`](https://github.com/tauri-apps/plugins-workspace/commit/4e2cef9b702bbbb9cf4ee17de50791cb21f1b2a4)([#593](https://github.com/tauri-apps/plugins-workspace/pull/593)) Update to alpha.12. + +## \[2.0.0-alpha.1] + +- [`d5a7c77`](https://github.com/tauri-apps/plugins-workspace/commit/d5a7c77a8d0e7912a6b07b22ed329004edd6e80b)([#545](https://github.com/tauri-apps/plugins-workspace/pull/545)) Fixes docs.rs build by enabling the `tauri/dox` feature flag. +- [`d74fc0a`](https://github.com/tauri-apps/plugins-workspace/commit/d74fc0a097996e90a37be8f57d50b7d1f6ca616f)([#555](https://github.com/tauri-apps/plugins-workspace/pull/555)) Update to alpha.11. + +## \[2.0.0-alpha.0] + +- [`717ae67`](https://github.com/tauri-apps/plugins-workspace/commit/717ae670978feb4492fac1f295998b93f2b9347f)([#371](https://github.com/tauri-apps/plugins-workspace/pull/371)) First v2 alpha release! diff --git a/plugins/localhost/Cargo.toml b/plugins/localhost/Cargo.toml new file mode 100644 index 0000000..c37476a --- /dev/null +++ b/plugins/localhost/Cargo.toml @@ -0,0 +1,26 @@ +[package] +name = "tauri-plugin-localhost" +version = "2.3.2" +description = "Expose your apps assets through a localhost server instead of the default custom protocol." +authors = { workspace = true } +license = { workspace = true } +edition = { workspace = true } +rust-version = { workspace = true } +repository = { workspace = true } +exclude = ["/banner.png"] + +[package.metadata.platforms.support] +windows = { level = "full", notes = "" } +linux = { level = "full", notes = "" } +macos = { level = "full", notes = "" } +android = { level = "none", notes = "" } +ios = { level = "none", notes = "" } + +[dependencies] +serde = { workspace = true } +serde_json = { workspace = true } +tauri = { workspace = true } +log = { workspace = true } +thiserror = { workspace = true } +tiny_http = "0.12" +http = "1" diff --git a/plugins/localhost/LICENSE.spdx b/plugins/localhost/LICENSE.spdx new file mode 100644 index 0000000..cdd0df5 --- /dev/null +++ b/plugins/localhost/LICENSE.spdx @@ -0,0 +1,20 @@ +SPDXVersion: SPDX-2.1 +DataLicense: CC0-1.0 +PackageName: tauri +DataFormat: SPDXRef-1 +PackageSupplier: Organization: The Tauri Programme in the Commons Conservancy +PackageHomePage: https://tauri.app +PackageLicenseDeclared: Apache-2.0 +PackageLicenseDeclared: MIT +PackageCopyrightText: 2019-2022, The Tauri Programme in the Commons Conservancy +PackageSummary: Tauri is a rust project that enables developers to make secure +and small desktop applications using a web frontend. + +PackageComment: The package includes the following libraries; see +Relationship information. + +Created: 2019-05-20T09:00:00Z +PackageDownloadLocation: git://github.com/tauri-apps/tauri +PackageDownloadLocation: git+https://github.com/tauri-apps/tauri.git +PackageDownloadLocation: git+ssh://github.com/tauri-apps/tauri.git +Creator: Person: Daniel Thompson-Yvetot \ No newline at end of file diff --git a/plugins/localhost/LICENSE_APACHE-2.0 b/plugins/localhost/LICENSE_APACHE-2.0 new file mode 100644 index 0000000..4947287 --- /dev/null +++ b/plugins/localhost/LICENSE_APACHE-2.0 @@ -0,0 +1,177 @@ + + 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 \ No newline at end of file diff --git a/plugins/localhost/LICENSE_MIT b/plugins/localhost/LICENSE_MIT new file mode 100644 index 0000000..4d75472 --- /dev/null +++ b/plugins/localhost/LICENSE_MIT @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2017 - Present Tauri Apps 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. \ No newline at end of file diff --git a/plugins/localhost/README.md b/plugins/localhost/README.md new file mode 100644 index 0000000..8ab13b2 --- /dev/null +++ b/plugins/localhost/README.md @@ -0,0 +1,108 @@ +![plugin-localhost](https://github.com/tauri-apps/plugins-workspace/raw/v2/plugins/localhost/banner.png) + +Expose your apps assets through a localhost server instead of the default custom protocol. + +| Platform | Supported | +| -------- | --------- | +| Linux | ✓ | +| Windows | ✓ | +| macOS | ✓ | +| Android | ✓ | +| iOS | ✓ | + +> Note: This plugins brings considerable security risks and you should only use it if you know what your are doing. If in doubt, use the default custom protocol implementation. + +## Install + +_This plugin requires a Rust version of at least **1.77.2**_ + +There are three general methods of installation that we can recommend. + +1. Use crates.io and npm (easiest, and requires you to trust that our publishing pipeline worked) +2. Pull sources directly from Github using git tags / revision hashes (most secure) +3. Git submodule install this repo in your tauri project and then use file protocol to ingest the source (most secure, but inconvenient to use) + +Install the Core plugin by adding the following to your `Cargo.toml` file: + +`src-tauri/Cargo.toml` + +```toml +[dependencies] +portpicker = "0.1" # used in the example to pick a random free port +tauri-plugin-localhost = "2.0.0" +# alternatively with Git: +tauri-plugin-localhost = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v2" } +``` + +## Usage + +First you need to register the core plugin with Tauri: + +`src-tauri/src/lib.rs` + +```rust +#[cfg(not(dev))] +use tauri::{ipc::CapabilityBuilder, Manager, Url}; +use tauri::{WebviewUrl, WebviewWindowBuilder}; + +#[cfg_attr(mobile, tauri::mobile_entry_point)] +pub fn run() { + let port = portpicker::pick_unused_port().expect("failed to find unused port"); + + tauri::Builder::default() + .plugin(tauri_plugin_localhost::Builder::new(port).build()) + .setup(move |app| { + // In `tauri dev` mode you usually use your dev server. + #[cfg(dev)] + let url = WebviewUrl::App(std::path::PathBuf::from("/")); + + #[cfg(not(dev))] + let url = { + let url: Url = format!("http://localhost:{}", port).parse().unwrap(); + + app.add_capability( + CapabilityBuilder::new("localhost") + .remote(url.to_string()) + .window("main"), + )?; + + WebviewUrl::External(url) + }; + + // This requires you to remove the window from tauri.conf.json + WebviewWindowBuilder::new(app, "main".to_string(), url) + .title("Localhost Example") + .build()?; + + Ok(()) + }) + .run(tauri::generate_context!()) + .expect("error while running tauri application"); +} +``` + +## Contributing + +PRs accepted. Please make sure to read the Contributing Guide before making a pull request. + +## Partners + + + + + + + +
+ + CrabNebula + +
+ +For the complete list of sponsors please visit our [website](https://tauri.app#sponsors) and [Open Collective](https://opencollective.com/tauri). + +## License + +Code: (c) 2015 - Present - The Tauri Programme within The Commons Conservancy. + +MIT or MIT/Apache 2.0 where applicable. diff --git a/plugins/localhost/SECURITY.md b/plugins/localhost/SECURITY.md new file mode 100644 index 0000000..4f09bba --- /dev/null +++ b/plugins/localhost/SECURITY.md @@ -0,0 +1,23 @@ +# Security Policy + +**Do not report security vulnerabilities through public GitHub issues.** + +**Please use the [Private Vulnerability Disclosure](https://docs.github.com/en/code-security/security-advisories/guidance-on-reporting-and-writing-information-about-vulnerabilities/privately-reporting-a-security-vulnerability#privately-reporting-a-security-vulnerability) feature of GitHub.** + +Include as much of the following information: + +- Type of issue (e.g. improper input parsing, privilege escalation, etc.) +- The location of the affected source code (tag/branch/commit or direct URL) +- Any special configuration required to reproduce the issue +- The distribution affected or used to help us with reproduction of the issue +- Step-by-step instructions to reproduce the issue +- Ideally a reproduction repository +- Impact of the issue, including how an attacker might exploit the issue + +We prefer to receive reports in English. + +## Contact + +Please disclose a vulnerability or security relevant issue here: [https://github.com/tauri-apps/plugins-workspace/security/advisories/new](https://github.com/tauri-apps/plugins-workspace/security/advisories/new). + +Alternatively, you can also contact us by email via [security@tauri.app](mailto:security@tauri.app). diff --git a/plugins/localhost/banner.png b/plugins/localhost/banner.png new file mode 100644 index 0000000000000000000000000000000000000000..3fe615c16e0417bfbe7ee5e1996d8709cf5fbd8b GIT binary patch literal 45169 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 Mv~FCjxQvbZ9~-R-ng9R* literal 0 HcmV?d00001 diff --git a/plugins/localhost/src/lib.rs b/plugins/localhost/src/lib.rs new file mode 100644 index 0000000..aba034b --- /dev/null +++ b/plugins/localhost/src/lib.rs @@ -0,0 +1,174 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +//! Expose your apps assets through a localhost server instead of the default custom protocol. +//! +//! **Note: This plugins brings considerable security risks and you should only use it if you know what your are doing. If in doubt, use the default custom protocol implementation.** + +#![doc( + html_logo_url = "https://github.com/tauri-apps/tauri/raw/dev/app-icon.png", + html_favicon_url = "https://github.com/tauri-apps/tauri/raw/dev/app-icon.png" +)] + +use std::collections::HashMap; + +use http::Uri; +use tauri::{ + plugin::{Builder as PluginBuilder, TauriPlugin}, + Runtime, +}; +use tiny_http::{Header, Response as HttpResponse, Server}; + +/// An incoming HTTP request received by the localhost server, passed to the +/// [`Builder::on_request`] hook. +pub struct Request { + url: String, +} + +impl Request { + /// The request URL (path and, if present, query string), exactly as sent by the client. + pub fn url(&self) -> &str { + &self.url + } +} + +/// The HTTP response the localhost server is about to send back for a matched asset. Passed +/// mutably to the [`Builder::on_request`] hook so it can add or override headers before the +/// response is written to the client. +pub struct Response { + headers: HashMap, +} + +impl Response { + /// Adds a header to the response, replacing any existing header with the same name. + pub fn add_header, V: Into>(&mut self, header: H, value: V) { + self.headers.insert(header.into(), value.into()); + } +} + +type OnRequest = Option>; + +/// Builds the localhost plugin. +/// +/// **Note: This plugin brings considerable security risks and you should only use it if you know +/// what you are doing. Because the server has no authentication, any local process can connect to +/// it and read the assets it serves. If in doubt, use the default custom protocol implementation.** +pub struct Builder { + port: u16, + host: Option, + on_request: OnRequest, +} + +impl Builder { + /// Creates a new [`Builder`] that will serve the app's assets on the given `port`, bound to + /// `localhost` unless [`Self::host`] is called. + /// + /// # Examples + /// + /// ```no_run + /// fn setup(builder: tauri::Builder) -> tauri::Builder { + /// builder.plugin( + /// tauri_plugin_localhost::Builder::new(9527) + /// .host("127.0.0.1") + /// .on_request(|request, response| { + /// println!("{}", request.url()); + /// }) + /// .build(), + /// ) + /// } + /// ``` + pub fn new(port: u16) -> Self { + Self { + port, + host: None, + on_request: None, + } + } + + /// Sets the host the localhost server binds to. Defaults to `localhost`. + pub fn host>(mut self, host: H) -> Self { + self.host = Some(host.into()); + self + } + + /// Sets a hook that is called for every request that resolves to a known frontend asset, + /// right before the response is sent. Use it to inspect the [`Request`] and add or override + /// headers on the [`Response`], for example to append custom CORS or caching headers. + pub fn on_request( + mut self, + f: F, + ) -> Self { + self.on_request.replace(Box::new(f)); + self + } + + /// Builds the plugin, ready to be registered with [`tauri::Builder::plugin`]. + /// + /// On setup it spawns a background thread that starts a `tiny_http` server bound to + /// `host:port`. For every incoming request whose path resolves to a known frontend asset, the + /// server responds with that asset's bytes, automatically setting the `Content-Type`, the + /// `Content-Security-Policy` (when the asset has one) and a `Cache-Control: no-cache` header, + /// then invoking the [`Self::on_request`] hook (if any) before writing the response. + /// + /// # Panics + /// + /// Panics on the background thread if the server fails to bind to `host:port`, or if it fails + /// to send a response for a request. + pub fn build(mut self) -> TauriPlugin { + let port = self.port; + let host = self.host.unwrap_or("localhost".to_string()); + let on_request = self.on_request.take(); + + PluginBuilder::new("localhost") + .setup(move |app, _api| { + let asset_resolver = app.asset_resolver(); + std::thread::spawn(move || { + let server = + Server::http(format!("{host}:{port}")).expect("Unable to spawn server"); + for req in server.incoming_requests() { + let path = req + .url() + .parse::() + .map(|uri| uri.path().into()) + .unwrap_or_else(|_| req.url().into()); + + #[allow(unused_mut)] + if let Some(mut asset) = asset_resolver.get(path) { + let request = Request { + url: req.url().into(), + }; + let mut response = Response { + headers: Default::default(), + }; + + response.add_header("Content-Type", asset.mime_type); + if let Some(csp) = asset.csp_header { + response + .headers + .insert("Content-Security-Policy".into(), csp); + } + + response + .headers + .insert("Cache-Control".into(), "no-cache".into()); + + if let Some(on_request) = &on_request { + on_request(&request, &mut response); + } + + let mut resp = HttpResponse::from_data(asset.bytes); + for (header, value) in response.headers { + if let Ok(h) = Header::from_bytes(header.as_bytes(), value) { + resp.add_header(h); + } + } + req.respond(resp).expect("unable to setup response"); + } + } + }); + Ok(()) + }) + .build() + } +} diff --git a/plugins/log/CHANGELOG.md b/plugins/log/CHANGELOG.md new file mode 100644 index 0000000..11aea9e --- /dev/null +++ b/plugins/log/CHANGELOG.md @@ -0,0 +1,188 @@ +# Changelog + +## [2.9.2] + +- [`2df3d936`](https://github.com/tauri-apps/plugins-workspace/commit/2df3d9368187e1d2ce736d3e0b2d4bdcae35b011) ([#3574](https://github.com/tauri-apps/plugins-workspace/pull/3574)) Log webview log target locations after 2 colons instead of 1. (e.g. `webview:myFunction@http://localhost:5173/src/myFile.ts?t=1787480723620:47:4` to `webview::myFunction@http://localhost:5173/src/myFile.ts?t=1787480723620:47:4`) + +## [2.9.1] + +- [`f8053e65`](https://github.com/tauri-apps/plugins-workspace/commit/f8053e659e4ccd85c1f52833411ff8417cbc5e69) ([#3527](https://github.com/tauri-apps/plugins-workspace/pull/3527) by [@Legend-Master](https://github.com/tauri-apps/plugins-workspace/../../Legend-Master)) Documented Cargo feature flags in each plugin's crate-level documentation. + +## \[2.9.0] + +- [`f08980f1`](https://github.com/tauri-apps/plugins-workspace/commit/f08980f123f191b9505bd290acd8fff0fdefeed9) ([#3445](https://github.com/tauri-apps/plugins-workspace/pull/3445) by [@bajoca05](https://github.com/tauri-apps/plugins-workspace/../../bajoca05)) Added the `FileOpenStrategy` for log rotation. It defaults to append into existing file if any (previous behaviour), and brings a new feature to create a new file per session: `FileOpenStrategy::Rotate`. +- [`254f222e`](https://github.com/tauri-apps/plugins-workspace/commit/254f222e0e2bc79370f977855b6b39d956d3b568) ([#3477](https://github.com/tauri-apps/plugins-workspace/pull/3477) by [@bajoca05](https://github.com/tauri-apps/plugins-workspace/../../bajoca05)) Migrated from the `log` crate's `kv_unstable` feature flag to the `kv` feature flag stabilized in `0.4.21`. +- [`0c23b8ec`](https://github.com/tauri-apps/plugins-workspace/commit/0c23b8ecfe7c2aca582a81ab7339b11e350b3cac) ([#3446](https://github.com/tauri-apps/plugins-workspace/pull/3446) by [@fee1-dead](https://github.com/tauri-apps/plugins-workspace/../../fee1-dead)) Removed an unused dependency `byte-unit`. + +## \[2.8.0] + +- [`2a625adf`](https://github.com/tauri-apps/plugins-workspace/commit/2a625adff30238904035b86b6e2db7595597e857) ([#3065](https://github.com/tauri-apps/plugins-workspace/pull/3065) by [@BinaryMuse](https://github.com/tauri-apps/plugins-workspace/../../BinaryMuse)) Allow specifying a log formatter per target using the `format` method on `Target`. +- [`ae278ddf`](https://github.com/tauri-apps/plugins-workspace/commit/ae278ddf60203da183fe2266c06a5bdeb909285c) ([#3110](https://github.com/tauri-apps/plugins-workspace/pull/3110) by [@liuzhch1](https://github.com/tauri-apps/plugins-workspace/../../liuzhch1)) Fix log file rotation when exceeding `max_file_size`. +- [`6de61f85`](https://github.com/tauri-apps/plugins-workspace/commit/6de61f854bee0c1d39e4ce5890b12754b931c163) ([#3113](https://github.com/tauri-apps/plugins-workspace/pull/3113) by [@lucasfernog](https://github.com/tauri-apps/plugins-workspace/../../lucasfernog)) Remove log delays for iOS simulators that are no longer necessary. + +## \[2.7.1] + +- [`93426f85`](https://github.com/tauri-apps/plugins-workspace/commit/93426f85120f49beb9f40222bff45185a32d54a9) Fixed an issue that caused docs.rs builds to fail. No user facing changes. + +## \[2.7.0] + +- [`625bb1c0`](https://github.com/tauri-apps/plugins-workspace/commit/625bb1c0965394b88522643731f78ccbcca84add) ([#2965](https://github.com/tauri-apps/plugins-workspace/pull/2965) by [@lucasfernog](https://github.com/tauri-apps/plugins-workspace/../../lucasfernog)) Re-export the log crate. + +## \[2.6.0] + +- [`f209b2f2`](https://github.com/tauri-apps/plugins-workspace/commit/f209b2f23cb29133c97ad5961fb46ef794dbe063) ([#2804](https://github.com/tauri-apps/plugins-workspace/pull/2804) by [@renovate](https://github.com/tauri-apps/plugins-workspace/../../renovate)) Updated tauri to 2.6 + +## \[2.5.1] + +### bug + +- [`9799f0db`](https://github.com/tauri-apps/plugins-workspace/commit/9799f0dbabea0b572a9b9111954fbf9aca63da71) ([#2802](https://github.com/tauri-apps/plugins-workspace/pull/2802) by [@lucasfernog](https://github.com/tauri-apps/plugins-workspace/../../lucasfernog)) Fixes iOS simulator still freezing sometimes due to early logging. + +## \[2.5.0] + +- [`106e46ed`](https://github.com/tauri-apps/plugins-workspace/commit/106e46ed5125be33d0427cab9c5c066802f68791) ([#677](https://github.com/tauri-apps/plugins-workspace/pull/677)) Added the `KeepSome` rotation strategy. Like `KeepAll` it will rename files when the max file size is exceeded but will keep only the specified amount of files around. + +## \[2.4.0] + +- [`c9b21f6f`](https://github.com/tauri-apps/plugins-workspace/commit/c9b21f6f4345806eff5f495885f20dea0082b7d7) ([#2625](https://github.com/tauri-apps/plugins-workspace/pull/2625) by [@lucasfernog](https://github.com/tauri-apps/plugins-workspace/../../lucasfernog)) Export the `LogLevel` type. +- [`9629c2f4`](https://github.com/tauri-apps/plugins-workspace/commit/9629c2f4f90a56b5c2d265d1d13d3af40fc0c525) ([#2600](https://github.com/tauri-apps/plugins-workspace/pull/2600) by [@exoego](https://github.com/tauri-apps/plugins-workspace/../../exoego)) Adds a new varient `TargetKind::Dispatch` that allows you to construct arbitrary log targets +- [`686a839c`](https://github.com/tauri-apps/plugins-workspace/commit/686a839c96fae1b0334f2df9dc76ca5cdbe00dbe) ([#2626](https://github.com/tauri-apps/plugins-workspace/pull/2626) by [@lucasfernog](https://github.com/tauri-apps/plugins-workspace/../../lucasfernog)) Fix iOS app stuck when using the iOS Simulator and the log plugin due to a deadlock when calling os_log too early. + +### feat + +- [`60fc35d3`](https://github.com/tauri-apps/plugins-workspace/commit/60fc35d35cccaf1654eceb4446ecf0f89dc15502) ([#2576](https://github.com/tauri-apps/plugins-workspace/pull/2576) by [@3lpsy](https://github.com/tauri-apps/plugins-workspace/../../3lpsy)) Add a `tracing` feature to the `log` plugin that emits log messages to the `tracing` system. + +## \[2.3.1] + +- [`1bb1ced5`](https://github.com/tauri-apps/plugins-workspace/commit/1bb1ced53820127204aa7adf57510c1cbce55e12) ([#2524](https://github.com/tauri-apps/plugins-workspace/pull/2524) by [@elwerene](https://github.com/tauri-apps/plugins-workspace/../../elwerene)) enable TargetKind::LogDir on mobile + +## \[2.3.0] + +### feat + +- [`02481501`](https://github.com/tauri-apps/plugins-workspace/commit/024815018fbc63a37afc716796a454925aa7d25e) ([#2377](https://github.com/tauri-apps/plugins-workspace/pull/2377) by [@3lpsy](https://github.com/tauri-apps/plugins-workspace/../../3lpsy)) Add a `is_skip_logger` flag to the Log Plugin `Builder` struct, a `skip_logger()` method to the Builder, and logic to avoid acquiring (creating) a logger and attaching it to the global logger. Since acquire_logger is pub, a `LoggerNotInitialized` is added and returned if it's called when the `is_skip_looger` flag is set. Overall, this feature permits a user to avoid calling `attach_logger` which can only be called once in a program's lifetime and allows the user to control the logger returned from `logger()`. Additionally, it also will allow users to generate multiple Tauri Mock apps in test suites that run and parallel and have the `log` plugin attached (assuming they use `skip_logger()`). + +## \[2.2.3] + +- [`1a984659`](https://github.com/tauri-apps/plugins-workspace/commit/1a9846599b6a71faf330845847a30f6bf9735898) ([#2469](https://github.com/tauri-apps/plugins-workspace/pull/2469) by [@FabianLars](https://github.com/tauri-apps/plugins-workspace/../../FabianLars)) Update `objc2` crate to 0.6. No user facing changes. + +## \[2.2.2] + +- [`6b4c3917`](https://github.com/tauri-apps/plugins-workspace/commit/6b4c3917389f4bc489d03b48a837557ac0584175) ([#2401](https://github.com/tauri-apps/plugins-workspace/pull/2401) by [@Seishiin](https://github.com/tauri-apps/plugins-workspace/../../Seishiin)) Fix timezone_strategy overwriting previously set LogLevels. + +## \[2.2.1] + +- [`784a54a3`](https://github.com/tauri-apps/plugins-workspace/commit/784a54a39094dfbaaa8dd123eb083c04dc6c3bb2) ([#2344](https://github.com/tauri-apps/plugins-workspace/pull/2344) by [@madsmtm](https://github.com/tauri-apps/plugins-workspace/../../madsmtm)) Use `objc2` instead of `objc`. + +## \[2.2.0] + +- [`3a79266b`](https://github.com/tauri-apps/plugins-workspace/commit/3a79266b8cf96a55b1ae6339d725567d45a44b1d) ([#2173](https://github.com/tauri-apps/plugins-workspace/pull/2173) by [@FabianLars](https://github.com/tauri-apps/plugins-workspace/../../FabianLars)) Bumped all plugins to `v2.2.0`. From now, the versions for the Rust and JavaScript packages of each plugin will be in sync with each other. + +## \[2.0.2] + +- [`69d508ee`](https://github.com/tauri-apps/plugins-workspace/commit/69d508ee6910ae4064f2398fbacb803b3944d6a8) ([#2157](https://github.com/tauri-apps/plugins-workspace/pull/2157) by [@Legend-Master](https://github.com/tauri-apps/plugins-workspace/../../Legend-Master)) Make log functions omit caller location when failed to parse it instead of throwing + +## \[2.0.1] + +- [`371a2f73`](https://github.com/tauri-apps/plugins-workspace/commit/371a2f7361e0b91cf66f1287ffb18b34414a6cb8) ([#2021](https://github.com/tauri-apps/plugins-workspace/pull/2021) by [@Legend-Master](https://github.com/tauri-apps/plugins-workspace/../../Legend-Master)) Make webview log target more consistent that it always starts with `webview` + +## \[2.0.2] + +- [`606fa08d`](https://github.com/tauri-apps/plugins-workspace/commit/606fa08dae1acd074b961fb360623f4c86f13ee8) ([#1997](https://github.com/tauri-apps/plugins-workspace/pull/1997) by [@renovate](https://github.com/tauri-apps/plugins-workspace/../../renovate)) **Potentially breaking:** Updated `fern` from 0.6 to 0.7. This is technically a breaking change because `fern` is re-exported in `tauri-plugin-log`. + +## \[2.0.1] + +- [`a1a82208`](https://github.com/tauri-apps/plugins-workspace/commit/a1a82208ed4ab87f83310be0dc95428aec9ab241) ([#1873](https://github.com/tauri-apps/plugins-workspace/pull/1873) by [@lucasfernog](https://github.com/tauri-apps/plugins-workspace/../../lucasfernog)) Downgrade MSRV to 1.77.2 to support Windows 7. + +## \[2.0.0] + +- [`e2c4dfb6`](https://github.com/tauri-apps/plugins-workspace/commit/e2c4dfb6af43e5dd8d9ceba232c315f5febd55c1) Update to tauri v2 stable release. + +## \[2.0.0-rc.1] + +- [`e2e97db5`](https://github.com/tauri-apps/plugins-workspace/commit/e2e97db51983267f5be84d4f6f0278d58834d1f5) ([#1701](https://github.com/tauri-apps/plugins-workspace/pull/1701) by [@lucasfernog](https://github.com/tauri-apps/plugins-workspace/../../lucasfernog)) Update to tauri 2.0.0-rc.8 + +## \[2.0.0-rc.1] + +- [`b9147758`](https://github.com/tauri-apps/plugins-workspace/commit/b914775898c2bee7ceb20bd17ee595005cd17a64) ([#1679](https://github.com/tauri-apps/plugins-workspace/pull/1679) by [@lucasfernog](https://github.com/tauri-apps/plugins-workspace/../../lucasfernog)) Explicitly set a minimum macOS version for the Swift package. + +## \[2.0.0-rc.0] + +- [`9887d1`](https://github.com/tauri-apps/plugins-workspace/commit/9887d14bd0e971c4c0f5c1188fc4005d3fc2e29e) Update to tauri RC. + +## \[2.0.0-beta.9] + +- [`99d6ac0f`](https://github.com/tauri-apps/plugins-workspace/commit/99d6ac0f9506a6a4a1aa59c728157190a7441af6) ([#1606](https://github.com/tauri-apps/plugins-workspace/pull/1606) by [@FabianLars](https://github.com/tauri-apps/plugins-workspace/../../FabianLars)) The JS packages now specify the *minimum* `@tauri-apps/api` version instead of a single exact version. +- [`6de87966`](https://github.com/tauri-apps/plugins-workspace/commit/6de87966ecc00ad9d91c25be452f1f46bd2b7e1f) ([#1597](https://github.com/tauri-apps/plugins-workspace/pull/1597) by [@Legend-Master](https://github.com/tauri-apps/plugins-workspace/../../Legend-Master)) Update to tauri beta.25. + +## \[2.0.0-beta.9] + +- [`20a1d24e`](https://github.com/tauri-apps/plugins-workspace/commit/20a1d24ee004e77c2d12a0e20d258ce120216ed1) ([#1579](https://github.com/tauri-apps/plugins-workspace/pull/1579) by [@lucasfernog](https://github.com/tauri-apps/plugins-workspace/../../lucasfernog)) Added `Builder::split` which returns the raw logger implementation so you can pipe to other loggers such as `multi_log` or `tauri-plugin-devtools`. + +## \[2.0.0-beta.8] + +- [`22a17980`](https://github.com/tauri-apps/plugins-workspace/commit/22a17980ff4f6f8c40adb1b8f4ffc6dae2fe7e30) ([#1537](https://github.com/tauri-apps/plugins-workspace/pull/1537) by [@lucasfernog](https://github.com/tauri-apps/plugins-workspace/../../lucasfernog)) Update to tauri beta.24. + +## \[2.0.0-beta.7] + +- [`76daee7a`](https://github.com/tauri-apps/plugins-workspace/commit/76daee7aafece34de3092c86e531cf9eb1138989) ([#1512](https://github.com/tauri-apps/plugins-workspace/pull/1512) by [@renovate](https://github.com/tauri-apps/plugins-workspace/../../renovate)) Update to tauri beta.23. + +## \[2.0.0-beta.6] + +- [`9013854f`](https://github.com/tauri-apps/plugins-workspace/commit/9013854f42a49a230b9dbb9d02774765528a923f)([#1382](https://github.com/tauri-apps/plugins-workspace/pull/1382)) Update to tauri beta.22. + +## \[2.0.0-beta.5] + +- [`430bd6f4`](https://github.com/tauri-apps/plugins-workspace/commit/430bd6f4f379bee5d232ae6b098ae131db7f178a)([#1363](https://github.com/tauri-apps/plugins-workspace/pull/1363)) Update to tauri beta.20. + +## \[2.0.0-beta.4] + +- [`bd1ed590`](https://github.com/tauri-apps/plugins-workspace/commit/bd1ed5903ffcce5500310dac1e59e8c67674ef1e)([#1237](https://github.com/tauri-apps/plugins-workspace/pull/1237)) Update to tauri beta.17. + +## \[2.0.0-beta.3] + +- [`ed46dca`](https://github.com/tauri-apps/plugins-workspace/commit/ed46dca74ff3947dbbcb26a7b571c129bf925698) Added `attachLogger` helper function to register a function that should be called for each log entry. + +## \[2.0.0-beta.3] + +- [`a04ea2f`](https://github.com/tauri-apps/plugins-workspace/commit/a04ea2f38294d5a3987578283badc8eec87a7752)([#1071](https://github.com/tauri-apps/plugins-workspace/pull/1071)) The global API script is now only added to the binary when the `withGlobalTauri` config is true. + +## \[2.0.0-beta.2] + +- [`99bea25`](https://github.com/tauri-apps/plugins-workspace/commit/99bea2559c2c0648c2519c50a18cd124dacef57b)([#1005](https://github.com/tauri-apps/plugins-workspace/pull/1005)) Update to tauri beta.8. + +## \[2.0.0-beta.1] + +- [`569defb`](https://github.com/tauri-apps/plugins-workspace/commit/569defbe9492e38938554bb7bdc1be9151456d21) Update to tauri beta.4. + +## \[2.0.0-beta.0] + +- [`d198c01`](https://github.com/tauri-apps/plugins-workspace/commit/d198c014863ee260cb0de88a14b7fc4356ef7474)([#862](https://github.com/tauri-apps/plugins-workspace/pull/862)) Update to tauri beta. + +## \[2.0.0-alpha.5] + +- [`387c2f9`](https://github.com/tauri-apps/plugins-workspace/commit/387c2f9e0ce4c75c07ffa3fd76391a25b58f5daf)([#802](https://github.com/tauri-apps/plugins-workspace/pull/802)) Update to @tauri-apps/api v2.0.0-alpha.13. + +## \[2.0.0-alpha.4] + +- [`387c2f9`](https://github.com/tauri-apps/plugins-workspace/commit/387c2f9e0ce4c75c07ffa3fd76391a25b58f5daf)([#802](https://github.com/tauri-apps/plugins-workspace/pull/802)) Update to @tauri-apps/api v2.0.0-alpha.12. + +## \[2.0.0-alpha.3] + +- [`e438e0a`](https://github.com/tauri-apps/plugins-workspace/commit/e438e0a62d4b430a5159f05f13ecd397dd891a0d)([#676](https://github.com/tauri-apps/plugins-workspace/pull/676)) Update to @tauri-apps/api v2.0.0-alpha.11. + +## \[2.0.0-alpha.2] + +- [`5c13736`](https://github.com/tauri-apps/plugins-workspace/commit/5c137365c60790e8d4037d449e8237aa3fffdab0)([#673](https://github.com/tauri-apps/plugins-workspace/pull/673)) Update to @tauri-apps/api v2.0.0-alpha.9. + +## \[2.0.0-alpha.2] + +- [`4e2cef9`](https://github.com/tauri-apps/plugins-workspace/commit/4e2cef9b702bbbb9cf4ee17de50791cb21f1b2a4)([#593](https://github.com/tauri-apps/plugins-workspace/pull/593)) Update to alpha.12. + +## \[2.0.0-alpha.1] + +- [`d74fc0a`](https://github.com/tauri-apps/plugins-workspace/commit/d74fc0a097996e90a37be8f57d50b7d1f6ca616f)([#555](https://github.com/tauri-apps/plugins-workspace/pull/555)) Update to alpha.11. + +## \[2.0.0-alpha.0] + +- [`717ae67`](https://github.com/tauri-apps/plugins-workspace/commit/717ae670978feb4492fac1f295998b93f2b9347f)([#371](https://github.com/tauri-apps/plugins-workspace/pull/371)) First v2 alpha release! diff --git a/plugins/log/Cargo.toml b/plugins/log/Cargo.toml new file mode 100644 index 0000000..1bf62bb --- /dev/null +++ b/plugins/log/Cargo.toml @@ -0,0 +1,58 @@ +[package] +name = "tauri-plugin-log" +version = "2.9.2" +description = "Configurable logging for your Tauri app." +authors = { workspace = true } +license = { workspace = true } +edition = { workspace = true } +rust-version = { workspace = true } +repository = { workspace = true } +links = "tauri-plugin-log" +exclude = [ + "/banner.png", + "/guest-js", + "/package.json", + "/rollup.config.js", + "/tsconfig.json", +] + +[package.metadata.platforms.support] +windows = { level = "full", notes = "" } +linux = { level = "full", notes = "" } +macos = { level = "full", notes = "" } +android = { level = "full", notes = "" } +ios = { level = "full", notes = "" } + +[build-dependencies] +tauri-plugin = { workspace = true, features = ["build"] } + +[dependencies] +serde = { workspace = true } +serde_json = { workspace = true } +tauri = { workspace = true } +thiserror = { workspace = true } +serde_repr = "0.1" +log = { workspace = true, features = ["kv"] } +time = { version = "0.3", features = [ + "formatting", + "local-offset", + "macros", + "parsing", +] } +fern = "0.7" +tracing = { workspace = true, optional = true } + +[target."cfg(target_os = \"android\")".dependencies] +android_logger = "0.15" + +[target."cfg(target_os = \"ios\")".dependencies] +swift-rs = "1" +objc2 = "0.6" +objc2-foundation = { version = "0.3", default-features = false, features = [ + "std", + "NSString", +] } + +[features] +colored = ["fern/colored"] +tracing = ["dep:tracing"] diff --git a/plugins/log/LICENSE.spdx b/plugins/log/LICENSE.spdx new file mode 100644 index 0000000..cdd0df5 --- /dev/null +++ b/plugins/log/LICENSE.spdx @@ -0,0 +1,20 @@ +SPDXVersion: SPDX-2.1 +DataLicense: CC0-1.0 +PackageName: tauri +DataFormat: SPDXRef-1 +PackageSupplier: Organization: The Tauri Programme in the Commons Conservancy +PackageHomePage: https://tauri.app +PackageLicenseDeclared: Apache-2.0 +PackageLicenseDeclared: MIT +PackageCopyrightText: 2019-2022, The Tauri Programme in the Commons Conservancy +PackageSummary: Tauri is a rust project that enables developers to make secure +and small desktop applications using a web frontend. + +PackageComment: The package includes the following libraries; see +Relationship information. + +Created: 2019-05-20T09:00:00Z +PackageDownloadLocation: git://github.com/tauri-apps/tauri +PackageDownloadLocation: git+https://github.com/tauri-apps/tauri.git +PackageDownloadLocation: git+ssh://github.com/tauri-apps/tauri.git +Creator: Person: Daniel Thompson-Yvetot \ No newline at end of file diff --git a/plugins/log/LICENSE_APACHE-2.0 b/plugins/log/LICENSE_APACHE-2.0 new file mode 100644 index 0000000..4947287 --- /dev/null +++ b/plugins/log/LICENSE_APACHE-2.0 @@ -0,0 +1,177 @@ + + 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 \ No newline at end of file diff --git a/plugins/log/LICENSE_MIT b/plugins/log/LICENSE_MIT new file mode 100644 index 0000000..4d75472 --- /dev/null +++ b/plugins/log/LICENSE_MIT @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2017 - Present Tauri Apps 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. \ No newline at end of file diff --git a/plugins/log/README.md b/plugins/log/README.md new file mode 100644 index 0000000..81ca9fd --- /dev/null +++ b/plugins/log/README.md @@ -0,0 +1,132 @@ +![plugin-log](https://github.com/tauri-apps/plugins-workspace/raw/v2/plugins/log/banner.png) + +Configurable logging for your Tauri app. + +| Platform | Supported | +| -------- | --------- | +| Linux | ✓ | +| Windows | ✓ | +| macOS | ✓ | +| Android | ✓ | +| iOS | ✓ | + +## Install + +_This plugin requires a Rust version of at least **1.77.2**_ + +There are three general methods of installation that we can recommend. + +1. Use crates.io and npm (easiest, and requires you to trust that our publishing pipeline worked) +2. Pull sources directly from Github using git tags / revision hashes (most secure) +3. Git submodule install this repo in your tauri project and then use file protocol to ingest the source (most secure, but inconvenient to use) + +Install the Core plugin by adding the following to your `Cargo.toml` file: + +`src-tauri/Cargo.toml` + +```toml +[dependencies] +tauri-plugin-log = "2.0.0" +# alternatively with Git: +tauri-plugin-log = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v2" } +``` + +If you want the single instance mechanism to only trigger for semver compatible instances of your apps, for example if you expect users to have multiple installations of your app installed, you can add `features = ["semver"]` to the dependency declaration in `Cargo.toml`. + +Then you can install the JavaScript Guest bindings using your preferred JavaScript package manager: + +```sh +pnpm add @tauri-apps/plugin-log +# or +npm add @tauri-apps/plugin-log +# or +yarn add @tauri-apps/plugin-log +``` + +## Usage + +First, you should enable the `log:default` capability: + +```json +{ + "$schema": "../gen/schemas/desktop-schema.json", + "identifier": "default", + "description": "Capability for the main window", + "windows": ["main"], + "permissions": [ + "core:default", + "opener:default", + "log:default" # add this! + ] +} +``` + +Then, you need to register the core plugin with Tauri: + +`src-tauri/src/lib.rs` + +```rust +use tauri_plugin_log::{Target, TargetKind}; + +fn main() { + tauri::Builder::default() + .plugin(tauri_plugin_log::Builder::new().targets([ + Target::new(TargetKind::Stdout), + Target::new(TargetKind::LogDir { file_name: None }), + Target::new(TargetKind::Webview), + ]).build()) + .run(tauri::generate_context!()) + .expect("error while running tauri application"); +} +``` + +Afterwards all the plugin's APIs are available through the JavaScript guest bindings: + +```javascript +import { trace, info, error, attachConsole } from '@tauri-apps/plugin-log' + +// with TargetKind::Webview enabled this function will print logs to the browser console +const detach = await attachConsole() + +trace('Trace') +info('Info') +error('Error') + +// detach the browser console from the log stream +detach() +``` + +To log from rust code, add the log crate to your `Cargo.toml`: + +```toml +[dependencies] +log = "^0.4" +``` + +Now, you can use the macros provided by the log crate to log messages from your backend. See the [docs](https://docs.rs/log/latest) for more details. + +## Contributing + +PRs accepted. Please make sure to read the Contributing Guide before making a pull request. + +## Partners + + + + + + + +
+ + CrabNebula + +
+ +For the complete list of sponsors please visit our [website](https://tauri.app#sponsors) and [Open Collective](https://opencollective.com/tauri). + +## License + +Code: (c) 2015 - Present - The Tauri Programme within The Commons Conservancy. + +MIT or MIT/Apache 2.0 where applicable. diff --git a/plugins/log/SECURITY.md b/plugins/log/SECURITY.md new file mode 100644 index 0000000..d013f6a --- /dev/null +++ b/plugins/log/SECURITY.md @@ -0,0 +1,56 @@ +# Security Policy + +**Do not report security vulnerabilities through public GitHub issues.** + +**Please use the [Private Vulnerability Disclosure](https://docs.github.com/en/code-security/security-advisories/guidance-on-reporting-and-writing-information-about-vulnerabilities/privately-reporting-a-security-vulnerability#privately-reporting-a-security-vulnerability) feature of GitHub.** + +Include as much of the following information: + +- Type of issue (e.g. improper input parsing, privilege escalation, etc.) +- The location of the affected source code (tag/branch/commit or direct URL) +- Any special configuration required to reproduce the issue +- The distribution affected or used to help us with reproduction of the issue +- Step-by-step instructions to reproduce the issue +- Ideally a reproduction repository +- Impact of the issue, including how an attacker might exploit the issue + +We prefer to receive reports in English. + +## Contact + +Please disclose a vulnerability or security relevant issue here: [https://github.com/tauri-apps/plugins-workspace/security/advisories/new](https://github.com/tauri-apps/plugins-workspace/security/advisories/new). + +Alternatively, you can also contact us by email via [security@tauri.app](mailto:security@tauri.app). + +## Threat Model + +### Security Assumptions + +- The log file interpreting applications are hardened, as input is not sanitized +- No log events in the rust core are leaked to the frontend unless explicitly configured to output to the `TargetKind::Webview` component +- The log events generated in the frontend can be accessed from everywhere in the frontend +- There is no secret censoring inbuilt and developers need to take care of what they log in their application + +### Threats + +#### Secret Leakage + +One possible threat you need to consider when using this plugin is that secrets +in logs can theoretically be leaked when the application's frontend gets compromised. + +For this threat to be possible all of the following requirements need to be fulfilled: + +- `TargetKind::Webview` enabled OR secrets stem from frontend logs +- Frontend application is compromised via something like XSS (cross-site-scripting) OR logs are directly exposed +- Logs contain secrets or sensitive information + +If these requirements are not met, the leakage should not be possible. + +#### Out Of Scope + +- Any exploits on the log viewer/file viewer accessing the logs + +## Best Practices + +Do not log secrets or sensitive values in your logging and ensure that the upstream crates are not leaking such values in their logging events. +Ensure that logs are sanitized or trusted before opening them with third party tools. diff --git a/plugins/log/api-iife.js b/plugins/log/api-iife.js new file mode 100644 index 0000000..18637ed --- /dev/null +++ b/plugins/log/api-iife.js @@ -0,0 +1 @@ +if("__TAURI__"in window){var __TAURI_PLUGIN_LOG__=function(e,n,o){"use strict";var r;async function a(e,o,r){const a=function(e){if(e){if(!e.startsWith("Error")){const n=e.split("\n").map(e=>e.split("@")).filter(([e,n])=>e.length>0&&"[native code]"!==n);return n[2]?.filter(e=>e.length>0).join("@")}{const n=e.split("\n"),o=n[3]?.trim();if(!o)return;const r=/at\s+(?.*?)\s+\((?.*?):(?\d+):(?\d+)\)/,a=o.match(r);if(a){const{functionName:e,fileName:n,lineNumber:o,columnNumber:r}=a.groups;return`${e}@${n}:${o}:${r}`}{const e=/at\s+(?.*?):(?\d+):(?\d+)/,n=o.match(e);if(n){const{fileName:e,lineNumber:o,columnNumber:r}=n.groups;return`@${e}:${o}:${r}`}}}}}((new Error).stack),{file:l,line:t,keyValues:i}=r??{};await n.invoke("plugin:log|log",{level:e,message:o,location:a,file:l,line:t,keyValues:i})}async function l(e){return await o.listen("log://log",n=>{const{level:o}=n.payload;let{message:r}=n.payload;r=r.replace(/[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g,""),e({message:r,level:o})})}return e.LogLevel=void 0,(r=e.LogLevel||(e.LogLevel={}))[r.Trace=1]="Trace",r[r.Debug=2]="Debug",r[r.Info=3]="Info",r[r.Warn=4]="Warn",r[r.Error=5]="Error",e.attachConsole=async function(){return await l(({level:n,message:o})=>{switch(n){case e.LogLevel.Trace:console.log(o);break;case e.LogLevel.Debug:console.debug(o);break;case e.LogLevel.Info:console.info(o);break;case e.LogLevel.Warn:console.warn(o);break;case e.LogLevel.Error:console.error(o);break;default:throw new Error(`unknown log level ${n}`)}})},e.attachLogger=l,e.debug=async function(n,o){await a(e.LogLevel.Debug,n,o)},e.error=async function(n,o){await a(e.LogLevel.Error,n,o)},e.info=async function(n,o){await a(e.LogLevel.Info,n,o)},e.trace=async function(n,o){await a(e.LogLevel.Trace,n,o)},e.warn=async function(n,o){await a(e.LogLevel.Warn,n,o)},e}({},window.__TAURI__.core,window.__TAURI__.event);Object.defineProperty(window.__TAURI__,"log",{value:__TAURI_PLUGIN_LOG__})} diff --git a/plugins/log/banner.png b/plugins/log/banner.png new file mode 100644 index 0000000000000000000000000000000000000000..c72d62cfee9457e43e2cf73a8c2202ad5d36789c GIT binary patch literal 38416 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 XQHLR_{rknQ00000NkvXXu0mjf +} + +/** + * The verbosity level of a log entry, matching the levels of the Rust `log` crate. + */ +export enum LogLevel { + /** + * The "trace" level. + * + * Designates very low priority, often extremely verbose, information. + */ + Trace = 1, + /** + * The "debug" level. + * + * Designates lower priority information. + */ + Debug, + /** + * The "info" level. + * + * Designates useful information. + */ + Info, + /** + * The "warn" level. + * + * Designates hazardous situations. + */ + Warn, + /** + * The "error" level. + * + * Designates very serious errors. + */ + Error +} + +function getCallerLocation(stack?: string) { + if (!stack) { + return + } + + if (stack.startsWith('Error')) { + // Assume it's Chromium V8 + // + // Error + // at baz (filename.js:10:15) + // at bar (filename.js:6:3) + // at foo (filename.js:2:3) + // at filename.js:13:1 + + const lines = stack.split('\n') + // Find the third line (caller's caller of the current location) + const callerLine = lines[3]?.trim() + if (!callerLine) { + return + } + + const regex = + /at\s+(?.*?)\s+\((?.*?):(?\d+):(?\d+)\)/ + const match = callerLine.match(regex) + + if (match) { + const { functionName, fileName, lineNumber, columnNumber } = + match.groups as { + functionName: string + fileName: string + lineNumber: string + columnNumber: string + } + return `${functionName}@${fileName}:${lineNumber}:${columnNumber}` + } else { + // Handle cases where the regex does not match (e.g., last line without function name) + const regexNoFunction = + /at\s+(?.*?):(?\d+):(?\d+)/ + const matchNoFunction = callerLine.match(regexNoFunction) + if (matchNoFunction) { + const { fileName, lineNumber, columnNumber } = + matchNoFunction.groups as { + fileName: string + lineNumber: string + columnNumber: string + } + return `@${fileName}:${lineNumber}:${columnNumber}` + } + } + } else { + // Assume it's Webkit JavaScriptCore, example: + // + // baz@filename.js:10:24 + // bar@filename.js:6:6 + // foo@filename.js:2:6 + // global code@filename.js:13:4 + + const traces = stack.split('\n').map((line) => line.split('@')) + const filtered = traces.filter(([name, location]) => { + return name.length > 0 && location !== '[native code]' + }) + // Find the third line (caller's caller of the current location) + return filtered[2]?.filter((v) => v.length > 0).join('@') + } +} + +async function log( + level: LogLevel, + message: string, + options?: LogOptions +): Promise { + const location = getCallerLocation(new Error().stack) + + const { file, line, keyValues } = options ?? {} + + await invoke('plugin:log|log', { + level, + message, + location, + file, + line, + keyValues + }) +} + +/** + * Logs a message at the error level. + * + * @example + * ```typescript + * import { error } from '@tauri-apps/plugin-log'; + * + * const err_info = "No connection"; + * const port = 22; + * + * error(`Error: ${err_info} on port ${port}`); + * ``` + * + * @param message the message to log. + * @param options additional metadata (file, line, key-values) to attach to the log entry. + * @since 2.0.0 + */ +export async function error( + message: string, + options?: LogOptions +): Promise { + await log(LogLevel.Error, message, options) +} + +/** + * Logs a message at the warn level. + * + * @example + * ```typescript + * import { warn } from '@tauri-apps/plugin-log'; + * + * const warn_description = "Invalid Input"; + * + * warn(`Warning! {warn_description}!`); + * ``` + * + * @param message the message to log. + * @param options additional metadata (file, line, key-values) to attach to the log entry. + * @since 2.0.0 + */ +export async function warn( + message: string, + options?: LogOptions +): Promise { + await log(LogLevel.Warn, message, options) +} + +/** + * Logs a message at the info level. + * + * @example + * ```typescript + * import { info } from '@tauri-apps/plugin-log'; + * + * const conn_info = { port: 40, speed: 3.20 }; + * + * info(`Connected to port {conn_info.port} at {conn_info.speed} Mb/s`); + * ``` + * + * @param message the message to log. + * @param options additional metadata (file, line, key-values) to attach to the log entry. + * @since 2.0.0 + */ +export async function info( + message: string, + options?: LogOptions +): Promise { + await log(LogLevel.Info, message, options) +} + +/** + * Logs a message at the debug level. + * + * @example + * ```typescript + * import { debug } from '@tauri-apps/plugin-log'; + * + * const pos = { x: 3.234, y: -1.223 }; + * + * debug(`New position: x: {pos.x}, y: {pos.y}`); + * ``` + * + * @param message the message to log. + * @param options additional metadata (file, line, key-values) to attach to the log entry. + * @since 2.0.0 + */ +export async function debug( + message: string, + options?: LogOptions +): Promise { + await log(LogLevel.Debug, message, options) +} + +/** + * Logs a message at the trace level. + * + * @example + * ```typescript + * import { trace } from '@tauri-apps/plugin-log'; + * + * let pos = { x: 3.234, y: -1.223 }; + * + * trace(`Position is: x: {pos.x}, y: {pos.y}`); + * ``` + * + * @param message the message to log. + * @param options additional metadata (file, line, key-values) to attach to the log entry. + * @since 2.0.0 + */ +export async function trace( + message: string, + options?: LogOptions +): Promise { + await log(LogLevel.Trace, message, options) +} + +interface RecordPayload { + level: LogLevel + message: string +} + +type LoggerFn = (fn: RecordPayload) => void + +/** + * Attaches a listener for the log, and calls the passed function for each log entry. + * + * @example + * ```typescript + * import { attachLogger } from '@tauri-apps/plugin-log'; + * + * const detach = await attachLogger(({ level, message }) => { + * console.log(`[${level}] ${message}`); + * }); + * + * // detach the listener later + * detach(); + * ``` + * + * @param fn a function to call for every log entry emitted by the Rust side. + * @returns a promise resolving to a function to cancel the listener. + * @since 2.0.0 + */ +export async function attachLogger(fn: LoggerFn): Promise { + return await listen('log://log', (event: Event) => { + const { level } = event.payload + let { message } = event.payload + + // Strip ANSI escape codes + message = message.replace( + // TODO: Investigate security/detect-unsafe-regex + // eslint-disable-next-line no-control-regex, security/detect-unsafe-regex + /[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g, + '' + ) + fn({ message, level }) + }) +} + +/** + * Attaches a listener that writes log entries to the console as they come in. + * + * @example + * ```typescript + * import { attachConsole } from '@tauri-apps/plugin-log'; + * + * const detach = await attachConsole(); + * + * // detach the listener later + * detach(); + * ``` + * + * @returns a promise resolving to a function to cancel the listener. + * @since 2.0.0 + */ +export async function attachConsole(): Promise { + return await attachLogger(({ level, message }: RecordPayload) => { + switch (level) { + case LogLevel.Trace: + console.log(message) + break + case LogLevel.Debug: + console.debug(message) + break + case LogLevel.Info: + console.info(message) + break + case LogLevel.Warn: + console.warn(message) + break + case LogLevel.Error: + console.error(message) + break + default: + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions + throw new Error(`unknown log level ${level}`) + } + }) +} diff --git a/plugins/log/ios/.gitignore b/plugins/log/ios/.gitignore new file mode 100644 index 0000000..5922fda --- /dev/null +++ b/plugins/log/ios/.gitignore @@ -0,0 +1,10 @@ +.DS_Store +/.build +/Packages +/*.xcodeproj +xcuserdata/ +DerivedData/ +.swiftpm/config/registries.json +.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata +.netrc +Package.resolved diff --git a/plugins/log/ios/Package.swift b/plugins/log/ios/Package.swift new file mode 100644 index 0000000..1571f22 --- /dev/null +++ b/plugins/log/ios/Package.swift @@ -0,0 +1,34 @@ +// swift-tools-version:5.3 +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +import PackageDescription + +let package = Package( + name: "tauri-plugin-log", + platforms: [ + .macOS(.v10_13), + .iOS(.v11), + ], + products: [ + // Products define the executables and libraries a package produces, and make them visible to other packages. + .library( + name: "tauri-plugin-log", + type: .static, + targets: ["tauri-plugin-log"]) + ], + dependencies: [ + .package(name: "Tauri", path: "../.tauri/tauri-api") + ], + targets: [ + // Targets are the basic building blocks of a package. A target can define a module or a test suite. + // Targets can depend on other targets in this package, and on products in packages this package depends on. + .target( + name: "tauri-plugin-log", + dependencies: [ + .byName(name: "Tauri") + ], + path: "Sources") + ] +) diff --git a/plugins/log/ios/README.md b/plugins/log/ios/README.md new file mode 100644 index 0000000..578d9ff --- /dev/null +++ b/plugins/log/ios/README.md @@ -0,0 +1,3 @@ +# Log + +Exposes a function log a message using the OSLog API. diff --git a/plugins/log/ios/Sources/LogPlugin.swift b/plugins/log/ios/Sources/LogPlugin.swift new file mode 100644 index 0000000..d1ced2c --- /dev/null +++ b/plugins/log/ios/Sources/LogPlugin.swift @@ -0,0 +1,18 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +import SwiftRs +import Tauri +import UIKit +import os.log + +@_cdecl("tauri_log") +func log(level: Int, message: NSString) { + switch level { + case 1: Logger.debug(message as String) + case 2: Logger.info(message as String) + case 3: Logger.error(message as String) + default: break + } +} diff --git a/plugins/log/package.json b/plugins/log/package.json new file mode 100644 index 0000000..b21ff6c --- /dev/null +++ b/plugins/log/package.json @@ -0,0 +1,30 @@ +{ + "name": "@tauri-apps/plugin-log", + "version": "2.9.2", + "description": "Configurable logging for your Tauri app.", + "license": "MIT OR Apache-2.0", + "authors": [ + "Tauri Programme within The Commons Conservancy" + ], + "repository": "https://github.com/tauri-apps/plugins-workspace", + "type": "module", + "types": "./dist-js/index.d.ts", + "main": "./dist-js/index.cjs", + "module": "./dist-js/index.js", + "exports": { + "types": "./dist-js/index.d.ts", + "import": "./dist-js/index.js", + "require": "./dist-js/index.cjs" + }, + "scripts": { + "build": "rollup -c" + }, + "files": [ + "dist-js", + "README.md", + "LICENSE" + ], + "dependencies": { + "@tauri-apps/api": "^2.11.0" + } +} diff --git a/plugins/log/permissions/autogenerated/commands/log.toml b/plugins/log/permissions/autogenerated/commands/log.toml new file mode 100644 index 0000000..ba36eff --- /dev/null +++ b/plugins/log/permissions/autogenerated/commands/log.toml @@ -0,0 +1,13 @@ +# Automatically generated - DO NOT EDIT! + +"$schema" = "../../schemas/schema.json" + +[[permission]] +identifier = "allow-log" +description = "Enables the log command without any pre-configured scope." +commands.allow = ["log"] + +[[permission]] +identifier = "deny-log" +description = "Denies the log command without any pre-configured scope." +commands.deny = ["log"] diff --git a/plugins/log/permissions/autogenerated/reference.md b/plugins/log/permissions/autogenerated/reference.md new file mode 100644 index 0000000..57d6c9f --- /dev/null +++ b/plugins/log/permissions/autogenerated/reference.md @@ -0,0 +1,43 @@ +## Default Permission + +Allows the log command + +#### This default permission set includes the following: + +- `allow-log` + +## Permission Table + + + + + + + + + + + + + + + + + +
IdentifierDescription
+ +`log:allow-log` + + + +Enables the log command without any pre-configured scope. + +
+ +`log:deny-log` + + + +Denies the log command without any pre-configured scope. + +
diff --git a/plugins/log/permissions/default.toml b/plugins/log/permissions/default.toml new file mode 100644 index 0000000..b2cb7c3 --- /dev/null +++ b/plugins/log/permissions/default.toml @@ -0,0 +1,4 @@ +"$schema" = "schemas/schema.json" +[default] +description = "Allows the log command" +permissions = ["allow-log"] diff --git a/plugins/log/permissions/schemas/schema.json b/plugins/log/permissions/schemas/schema.json new file mode 100644 index 0000000..cfee7e7 --- /dev/null +++ b/plugins/log/permissions/schemas/schema.json @@ -0,0 +1,318 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "PermissionFile", + "description": "Permission file that can define a default permission, a set of permissions or a list of inlined permissions.", + "type": "object", + "properties": { + "default": { + "description": "The default permission set for the plugin", + "anyOf": [ + { + "$ref": "#/definitions/DefaultPermission" + }, + { + "type": "null" + } + ] + }, + "set": { + "description": "A list of permissions sets defined", + "type": "array", + "items": { + "$ref": "#/definitions/PermissionSet" + } + }, + "permission": { + "description": "A list of inlined permissions", + "default": [], + "type": "array", + "items": { + "$ref": "#/definitions/Permission" + } + } + }, + "definitions": { + "DefaultPermission": { + "description": "The default permission set of the plugin.\n\nWorks similarly to a permission with the \"default\" identifier.", + "type": "object", + "required": [ + "permissions" + ], + "properties": { + "version": { + "description": "The version of the permission.", + "type": [ + "integer", + "null" + ], + "format": "uint64", + "minimum": 1.0 + }, + "description": { + "description": "Human-readable description of what the permission does. Tauri convention is to use `

` headings in markdown content for Tauri documentation generation purposes.", + "type": [ + "string", + "null" + ] + }, + "permissions": { + "description": "All permissions this set contains.", + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "PermissionSet": { + "description": "A set of direct permissions grouped together under a new name.", + "type": "object", + "required": [ + "description", + "identifier", + "permissions" + ], + "properties": { + "identifier": { + "description": "A unique identifier for the permission.", + "type": "string" + }, + "description": { + "description": "Human-readable description of what the permission does.", + "type": "string" + }, + "permissions": { + "description": "All permissions this set contains.", + "type": "array", + "items": { + "$ref": "#/definitions/PermissionKind" + } + } + } + }, + "Permission": { + "description": "Descriptions of explicit privileges of commands.\n\nIt can enable commands to be accessible in the frontend of the application.\n\nIf the scope is defined it can be used to fine grain control the access of individual or multiple commands.", + "type": "object", + "required": [ + "identifier" + ], + "properties": { + "version": { + "description": "The version of the permission.", + "type": [ + "integer", + "null" + ], + "format": "uint64", + "minimum": 1.0 + }, + "identifier": { + "description": "A unique identifier for the permission.", + "type": "string" + }, + "description": { + "description": "Human-readable description of what the permission does. Tauri internal convention is to use `

` headings in markdown content for Tauri documentation generation purposes.", + "type": [ + "string", + "null" + ] + }, + "commands": { + "description": "Allowed or denied commands when using this permission.", + "default": { + "allow": [], + "deny": [] + }, + "allOf": [ + { + "$ref": "#/definitions/Commands" + } + ] + }, + "scope": { + "description": "Allowed or denied scoped when using this permission.", + "allOf": [ + { + "$ref": "#/definitions/Scopes" + } + ] + }, + "platforms": { + "description": "Target platforms this permission applies. By default all platforms are affected by this permission.", + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/Target" + } + } + } + }, + "Commands": { + "description": "Allowed and denied commands inside a permission.\n\nIf two commands clash inside of `allow` and `deny`, it should be denied by default.", + "type": "object", + "properties": { + "allow": { + "description": "Allowed command.", + "default": [], + "type": "array", + "items": { + "type": "string" + } + }, + "deny": { + "description": "Denied command, which takes priority.", + "default": [], + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "Scopes": { + "description": "An argument for fine grained behavior control of Tauri commands.\n\nIt can be of any serde serializable type and is used to allow or prevent certain actions inside a Tauri command. The configured scope is passed to the command and will be enforced by the command implementation.\n\n## Example\n\n```json { \"allow\": [{ \"path\": \"$HOME/**\" }], \"deny\": [{ \"path\": \"$HOME/secret.txt\" }] } ```", + "type": "object", + "properties": { + "allow": { + "description": "Data that defines what is allowed by the scope.", + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/Value" + } + }, + "deny": { + "description": "Data that defines what is denied by the scope. This should be prioritized by validation logic.", + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/Value" + } + } + } + }, + "Value": { + "description": "All supported ACL values.", + "anyOf": [ + { + "description": "Represents a null JSON value.", + "type": "null" + }, + { + "description": "Represents a [`bool`].", + "type": "boolean" + }, + { + "description": "Represents a valid ACL [`Number`].", + "allOf": [ + { + "$ref": "#/definitions/Number" + } + ] + }, + { + "description": "Represents a [`String`].", + "type": "string" + }, + { + "description": "Represents a list of other [`Value`]s.", + "type": "array", + "items": { + "$ref": "#/definitions/Value" + } + }, + { + "description": "Represents a map of [`String`] keys to [`Value`]s.", + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/Value" + } + } + ] + }, + "Number": { + "description": "A valid ACL number.", + "anyOf": [ + { + "description": "Represents an [`i64`].", + "type": "integer", + "format": "int64" + }, + { + "description": "Represents a [`f64`].", + "type": "number", + "format": "double" + } + ] + }, + "Target": { + "description": "Platform target.", + "oneOf": [ + { + "description": "MacOS.", + "type": "string", + "enum": [ + "macOS" + ] + }, + { + "description": "Windows.", + "type": "string", + "enum": [ + "windows" + ] + }, + { + "description": "Linux.", + "type": "string", + "enum": [ + "linux" + ] + }, + { + "description": "Android.", + "type": "string", + "enum": [ + "android" + ] + }, + { + "description": "iOS.", + "type": "string", + "enum": [ + "iOS" + ] + } + ] + }, + "PermissionKind": { + "type": "string", + "oneOf": [ + { + "description": "Enables the log command without any pre-configured scope.", + "type": "string", + "const": "allow-log", + "markdownDescription": "Enables the log command without any pre-configured scope." + }, + { + "description": "Denies the log command without any pre-configured scope.", + "type": "string", + "const": "deny-log", + "markdownDescription": "Denies the log command without any pre-configured scope." + }, + { + "description": "Allows the log command\n#### This default permission set includes:\n\n- `allow-log`", + "type": "string", + "const": "default", + "markdownDescription": "Allows the log command\n#### This default permission set includes:\n\n- `allow-log`" + } + ] + } + } +} \ No newline at end of file diff --git a/plugins/log/rollup.config.js b/plugins/log/rollup.config.js new file mode 100644 index 0000000..1f349ec --- /dev/null +++ b/plugins/log/rollup.config.js @@ -0,0 +1,7 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +import { createConfig } from '../../shared/rollup.config.js' + +export default createConfig() diff --git a/plugins/log/src/commands.rs b/plugins/log/src/commands.rs new file mode 100644 index 0000000..e64c30a --- /dev/null +++ b/plugins/log/src/commands.rs @@ -0,0 +1,73 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use std::collections::HashMap; + +use log::RecordBuilder; + +use crate::{LogLevel, WEBVIEW_TARGET}; + +#[tauri::command] +pub fn log( + level: LogLevel, + message: String, + location: Option<&str>, + file: Option<&str>, + line: Option, + key_values: Option>, +) { + let level = log::Level::from(level); + + let target = if let Some(location) = location { + format!("{WEBVIEW_TARGET}::{location}") + } else { + WEBVIEW_TARGET.to_owned() + }; + + let mut builder = RecordBuilder::new(); + builder.level(level).target(&target).file(file).line(line); + + let key_values = key_values.unwrap_or_default(); + let mut kv = HashMap::new(); + for (k, v) in key_values.iter() { + kv.insert(k.as_str(), v.as_str()); + } + builder.key_values(&kv); + #[cfg(feature = "tracing")] + emit_trace(level, &message, location, file, line, &kv); + + log::logger().log(&builder.args(format_args!("{message}")).build()); +} + +// Target becomes default and location is added as a parameter +#[cfg(feature = "tracing")] +fn emit_trace( + level: log::Level, + message: &String, + location: Option<&str>, + file: Option<&str>, + line: Option, + kv: &HashMap<&str, &str>, +) { + macro_rules! emit_event { + ($level:expr) => { + tracing::event!( + target: WEBVIEW_TARGET, + $level, + message = %message, + location = location, + file, + line, + ?kv + ) + }; + } + match level { + log::Level::Error => emit_event!(tracing::Level::ERROR), + log::Level::Warn => emit_event!(tracing::Level::WARN), + log::Level::Info => emit_event!(tracing::Level::INFO), + log::Level::Debug => emit_event!(tracing::Level::DEBUG), + log::Level::Trace => emit_event!(tracing::Level::TRACE), + } +} diff --git a/plugins/log/src/lib.rs b/plugins/log/src/lib.rs new file mode 100644 index 0000000..18e8727 --- /dev/null +++ b/plugins/log/src/lib.rs @@ -0,0 +1,925 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +//! Logging for Tauri applications. +//! +//! ## Cargo features +//! +//! - **colored**: Enables `Builder::with_colors`, which uses `fern`'s `colored` feature for ANSI-colored outputs. +//! - **tracing**: Emit both log and tracing for the JavaScript log commands. + +#![doc( + html_logo_url = "https://github.com/tauri-apps/tauri/raw/dev/app-icon.png", + html_favicon_url = "https://github.com/tauri-apps/tauri/raw/dev/app-icon.png" +)] + +use fern::{Filter, FormatCallback}; +use log::{LevelFilter, Record}; +use serde::Serialize; +use serde_repr::{Deserialize_repr, Serialize_repr}; +use std::borrow::Cow; +use std::fs::OpenOptions; +use std::io::Write; +use std::{ + fmt::Arguments, + fs::{self, File}, + iter::FromIterator, + path::{Path, PathBuf}, +}; +use tauri::{ + plugin::{self, TauriPlugin}, + Manager, Runtime, +}; +use tauri::{AppHandle, Emitter}; +use time::{macros::format_description, OffsetDateTime}; + +pub use fern; +pub use log; + +mod commands; + +/// The log target prefix used for log records emitted through the JavaScript `trace`/`debug`/`info`/`warn`/`error` +/// bindings, optionally followed by `::{location}` when the caller's source location could be determined. +pub const WEBVIEW_TARGET: &str = "webview"; + +#[cfg(target_os = "ios")] +mod ios { + swift_rs::swift!(pub fn tauri_log( + level: u8, message: *const std::ffi::c_void + )); +} + +const DEFAULT_MAX_FILE_SIZE: u64 = 40_000; +const DEFAULT_ROTATION_STRATEGY: RotationStrategy = RotationStrategy::KeepOne; +const DEFAULT_TIMEZONE_STRATEGY: TimezoneStrategy = TimezoneStrategy::UseUtc; +const DEFAULT_FILE_OPEN_STRATEGY: FileOpenStrategy = FileOpenStrategy::Append; +const DEFAULT_LOG_TARGETS: [Target; 2] = [ + Target::new(TargetKind::Stdout), + Target::new(TargetKind::LogDir { file_name: None }), +]; +const LOG_DATE_FORMAT: &[time::format_description::FormatItem<'_>] = + format_description!("[year]-[month]-[day]_[hour]-[minute]-[second]"); + +/// The error type returned by this plugin's fallible operations. +#[derive(Debug, thiserror::Error)] +pub enum Error { + /// Forwarded from a call into the Tauri API, e.g. resolving the app log directory. + #[error(transparent)] + Tauri(#[from] tauri::Error), + /// Forwarded from a filesystem operation performed while creating, writing to or rotating a log file. + #[error(transparent)] + Io(#[from] std::io::Error), + /// Forwarded from formatting a timestamp with the configured [`TimezoneStrategy`]. + #[error(transparent)] + TimeFormat(#[from] time::error::Format), + /// Forwarded from parsing an invalid `time` format description. + #[error(transparent)] + InvalidFormatDescription(#[from] time::error::InvalidFormatDescription), + /// Returned by [`Builder::split`] when [`Builder::skip_logger`] was called on the builder, since no logger was + /// created for it to return. + #[error("Internal logger disabled and cannot be acquired or attached")] + LoggerNotInitialized, +} + +/// An enum representing the available verbosity levels of the logger. +/// +/// It is very similar to the [`log::Level`], but serializes to unsigned ints instead of strings. +#[derive(Debug, Clone, Deserialize_repr, Serialize_repr)] +#[repr(u16)] +pub enum LogLevel { + /// The "trace" level. + /// + /// Designates very low priority, often extremely verbose, information. + Trace = 1, + /// The "debug" level. + /// + /// Designates lower priority information. + Debug, + /// The "info" level. + /// + /// Designates useful information. + Info, + /// The "warn" level. + /// + /// Designates hazardous situations. + Warn, + /// The "error" level. + /// + /// Designates very serious errors. + Error, +} + +impl From for log::Level { + fn from(log_level: LogLevel) -> Self { + match log_level { + LogLevel::Trace => log::Level::Trace, + LogLevel::Debug => log::Level::Debug, + LogLevel::Info => log::Level::Info, + LogLevel::Warn => log::Level::Warn, + LogLevel::Error => log::Level::Error, + } + } +} + +impl From for LogLevel { + fn from(log_level: log::Level) -> Self { + match log_level { + log::Level::Trace => LogLevel::Trace, + log::Level::Debug => LogLevel::Debug, + log::Level::Info => LogLevel::Info, + log::Level::Warn => LogLevel::Warn, + log::Level::Error => LogLevel::Error, + } + } +} + +/// The strategy applied to a log file when it exceeds [`Builder::max_file_size`]. +#[derive(Debug, Clone)] +pub enum RotationStrategy { + /// Will keep all the logs, renaming them to include the date. + KeepAll, + /// Will only keep the most recent log up to its maximal size. + KeepOne, + /// Will keep some of the most recent logs, renaming them to include the date. + KeepSome(usize), +} + +/// The timezone used to compute the current time for log message timestamps and rotated log file names. +#[derive(Debug, Clone)] +pub enum TimezoneStrategy { + /// Use UTC time. + UseUtc, + /// Use the local timezone. Falls back to UTC if the local offset cannot be determined. + UseLocal, +} + +impl TimezoneStrategy { + /// Returns the current time according to this strategy. + pub fn get_now(&self) -> OffsetDateTime { + match self { + TimezoneStrategy::UseUtc => OffsetDateTime::now_utc(), + TimezoneStrategy::UseLocal => { + OffsetDateTime::now_local().unwrap_or_else(|_| OffsetDateTime::now_utc()) + } // Fallback to UTC since Rust cannot determine local timezone + } + } +} + +/// The strategy used to open a log file when a [`TargetKind::Folder`] or [`TargetKind::LogDir`] target is set up. +#[derive(Debug, Clone, PartialEq)] +pub enum FileOpenStrategy { + /// Open existing file from last session and append, if any. + Append, + /// Create a new file on each session start, rotating the last session if any. + Rotate, +} + +/// A custom log writer that rotates the log file when it exceeds specified size. +struct RotatingFile { + dir: PathBuf, + file_name: String, + path: PathBuf, + /// Maximum file size before rotating in bytes + max_size: u64, + /// Current file size in bytes + current_size: u64, + rotation_strategy: RotationStrategy, + timezone_strategy: TimezoneStrategy, + file_open_strategy: FileOpenStrategy, + inner: Option, + buffer: Vec, +} + +impl RotatingFile { + pub fn new( + dir: impl AsRef, + file_name: String, + max_size: u64, + rotation_strategy: RotationStrategy, + timezone_strategy: TimezoneStrategy, + file_open_strategy: FileOpenStrategy, + ) -> Result { + let dir = dir.as_ref().to_path_buf(); + let path = dir.join(&file_name).with_extension("log"); + + let mut rotator = Self { + dir, + file_name, + path, + max_size, + current_size: 0, + rotation_strategy, + timezone_strategy, + file_open_strategy, + inner: None, + buffer: Vec::new(), + }; + + rotator.open_file()?; + if rotator.current_size >= rotator.max_size + || (rotator.current_size > 0 && rotator.file_open_strategy == FileOpenStrategy::Rotate) + { + rotator.rotate()?; + } + if let RotationStrategy::KeepSome(keep_count) = rotator.rotation_strategy { + rotator.remove_old_files(keep_count)?; + } + + Ok(rotator) + } + + fn open_file(&mut self) -> Result<(), Error> { + let file = OpenOptions::new() + .create(true) + .append(true) + .open(&self.path)?; + self.current_size = file.metadata()?.len(); + self.inner = Some(file); + Ok(()) + } + + fn rotate(&mut self) -> Result<(), Error> { + if let Some(mut file) = self.inner.take() { + let _ = file.flush(); + } + if self.path.exists() { + match self.rotation_strategy { + RotationStrategy::KeepAll => { + self.rename_file_to_dated()?; + } + RotationStrategy::KeepSome(keep_count) => { + // remove_old_files excludes the active file. + // So we need to keep (keep_count - 1) archived files to make room for the one we are about to archive. + self.remove_old_files(keep_count - 1)?; + self.rename_file_to_dated()?; + } + RotationStrategy::KeepOne => { + fs::remove_file(&self.path)?; + } + } + } + self.open_file()?; + Ok(()) + } + + /// Remove old log files until the number of old log files is equal to the keep_count, + /// the current active log file is not included in the keep_count. + fn remove_old_files(&self, keep_count: usize) -> Result<(), Error> { + let mut files = fs::read_dir(&self.dir)? + .filter_map(|entry| { + let entry = entry.ok()?; + let path = entry.path(); + let old_file_name = path.file_name()?.to_string_lossy().into_owned(); + if old_file_name.starts_with(&self.file_name) + // exclude the current active file + && old_file_name != format!("{}.log", self.file_name) + { + let date = old_file_name + .strip_prefix(&self.file_name)? + .strip_prefix("_")? + .strip_suffix(".log")?; + Some((path, date.to_string())) + } else { + None + } + }) + .collect::>(); + + files.sort_by(|a, b| a.1.cmp(&b.1)); + + if files.len() > keep_count { + let files_to_remove = files.len() - keep_count; + for (old_log_path, _) in files.iter().take(files_to_remove) { + fs::remove_file(old_log_path)?; + } + } + Ok(()) + } + + fn rename_file_to_dated(&self) -> Result<(), Error> { + let to = self.dir.join(format!( + "{}_{}.log", + self.file_name, + self.timezone_strategy + .get_now() + .format(LOG_DATE_FORMAT) + .unwrap(), + )); + if to.is_file() { + // designated rotated log file name already exists + // highly unlikely but defensively handle anyway by adding .bak to filename + let mut to_bak = to.clone(); + to_bak.set_file_name(format!( + "{}.bak", + to_bak.file_name().unwrap().to_string_lossy() + )); + fs::rename(&to, to_bak)?; + } + fs::rename(&self.path, &to)?; + Ok(()) + } +} + +impl Write for RotatingFile { + fn write(&mut self, buf: &[u8]) -> std::io::Result { + self.buffer.extend_from_slice(buf); + Ok(buf.len()) + } + + fn flush(&mut self) -> std::io::Result<()> { + if self.buffer.is_empty() { + return Ok(()); + } + if self.inner.is_none() { + self.open_file().map_err(std::io::Error::other)?; + } + + if self.current_size != 0 && self.current_size + (self.buffer.len() as u64) > self.max_size + { + self.rotate().map_err(std::io::Error::other)?; + } + + if let Some(file) = self.inner.as_mut() { + file.write_all(&self.buffer)?; + self.current_size += self.buffer.len() as u64; + file.flush()?; + } + self.buffer.clear(); + Ok(()) + } +} + +#[derive(Debug, Serialize, Clone)] +struct RecordPayload { + message: String, + level: LogLevel, +} + +/// An enum representing the available targets of the logger. +pub enum TargetKind { + /// Print logs to stdout. + Stdout, + /// Print logs to stderr. + Stderr, + /// Write logs to the given directory. + /// + /// The plugin will ensure the directory exists before writing logs. + Folder { + /// Path of the directory to write the log file to. + path: PathBuf, + /// Name of the log file, without extension. Defaults to the app's package name when `None`. + file_name: Option, + }, + /// Write logs to the OS specific logs directory. + /// + /// ### Platform-specific + /// + /// |Platform | Value | Example | + /// | --------- | ----------------------------------------------------------------------------------------- | ----------------------------------------------------------- | + /// | Linux | `$XDG_DATA_HOME/{bundleIdentifier}/logs` or `$HOME/.local/share/{bundleIdentifier}/logs` | `/home/alice/.local/share/com.tauri.dev/logs` | + /// | macOS/iOS | `{homeDir}/Library/Logs/{bundleIdentifier}` | `/Users/Alice/Library/Logs/com.tauri.dev` | + /// | Windows | `{FOLDERID_LocalAppData}/{bundleIdentifier}/logs` | `C:\Users\Alice\AppData\Local\com.tauri.dev\logs` | + /// | Android | `{ConfigDir}/logs` | `/data/data/com.tauri.dev/files/logs` | + LogDir { + /// Name of the log file, without extension. Defaults to the app's package name when `None`. + file_name: Option, + }, + /// Forward logs to the webview (via the `log://log` event). + /// + /// This requires the webview to subscribe to log events, via this plugins `attachConsole` function. + Webview, + /// Send logs to a [`fern::Dispatch`] + /// + /// You can use this to construct arbitrary log targets. + Dispatch(fern::Dispatch), +} + +type Formatter = dyn Fn(FormatCallback, &Arguments, &Record) + Send + Sync + 'static; + +/// A log target. +pub struct Target { + kind: TargetKind, + filters: Vec>, + formatter: Option>, +} + +impl Target { + /// Creates a new [`Target`] of the given [`TargetKind`], with no filters and the [`Builder`]'s formatter. + #[inline] + pub const fn new(kind: TargetKind) -> Self { + Self { + kind, + filters: Vec::new(), + formatter: None, + } + } + + /// Adds a filter that can reject log records from being sent to this target. Multiple filters may be added by + /// calling this method more than once. + #[inline] + pub fn filter(mut self, filter: F) -> Self + where + F: Fn(&log::Metadata) -> bool + Send + Sync + 'static, + { + self.filters.push(Box::new(filter)); + self + } + + /// Sets a formatter for log records sent to this target, overriding the [`Builder`]'s formatter for this + /// target only. + #[inline] + pub fn format(mut self, formatter: F) -> Self + where + F: Fn(FormatCallback, &Arguments, &Record) + Send + Sync + 'static, + { + self.formatter.replace(Box::new(formatter)); + self + } +} + +/// Builds the log plugin, configuring targets, rotation, formatting and level filters before attaching the +/// resulting logger as the global logger for the [`log`] crate. +pub struct Builder { + dispatch: fern::Dispatch, + rotation_strategy: RotationStrategy, + timezone_strategy: TimezoneStrategy, + file_open_strategy: FileOpenStrategy, + max_file_size: u128, + targets: Vec, + is_skip_logger: bool, +} + +impl Default for Builder { + fn default() -> Self { + #[cfg(desktop)] + let format = format_description!("[[[year]-[month]-[day]][[[hour]:[minute]:[second]]"); + let dispatch = fern::Dispatch::new().format(move |out, message, record| { + out.finish( + #[cfg(mobile)] + format_args!("[{}] {}", record.target(), message), + #[cfg(desktop)] + format_args!( + "{}[{}][{}] {}", + DEFAULT_TIMEZONE_STRATEGY.get_now().format(&format).unwrap(), + record.target(), + record.level(), + message + ), + ) + }); + Self { + dispatch, + rotation_strategy: DEFAULT_ROTATION_STRATEGY, + timezone_strategy: DEFAULT_TIMEZONE_STRATEGY, + file_open_strategy: DEFAULT_FILE_OPEN_STRATEGY, + max_file_size: DEFAULT_MAX_FILE_SIZE as u128, + targets: DEFAULT_LOG_TARGETS.into(), + is_skip_logger: false, + } + } +} + +impl Builder { + /// Creates a new [`Builder`] with the default configuration: targets [`TargetKind::Stdout`] and + /// [`TargetKind::LogDir`] (using the app's package name as the file name), [`RotationStrategy::KeepOne`], + /// [`TimezoneStrategy::UseUtc`], [`FileOpenStrategy::Append`], a maximum log file size of `40_000` bytes + /// (see [`Self::max_file_size`]) and the default `fern` formatter. + pub fn new() -> Self { + Default::default() + } + + /// Sets the [`RotationStrategy`]. + /// + /// Default is [`RotationStrategy::KeepOne`] + pub fn rotation_strategy(mut self, rotation_strategy: RotationStrategy) -> Self { + self.rotation_strategy = rotation_strategy; + self + } + + /// Sets the [`TimezoneStrategy`]. + /// Calling this method overrides the format set in [`Self::format`]. + /// + /// Default is [`TimezoneStrategy::UseUtc`] + pub fn timezone_strategy(mut self, timezone_strategy: TimezoneStrategy) -> Self { + self.timezone_strategy = timezone_strategy.clone(); + + let format = format_description!("[[[year]-[month]-[day]][[[hour]:[minute]:[second]]"); + self.dispatch = self.dispatch.format(move |out, message, record| { + out.finish(format_args!( + "{}[{}][{}] {}", + timezone_strategy.get_now().format(&format).unwrap(), + record.level(), + record.target(), + message + )) + }); + self + } + + /// Sets the strategy to open the log file. + /// + /// The default is [`FileOpenStrategy::Append`]. + pub fn file_open_strategy(mut self, file_open_strategy: FileOpenStrategy) -> Self { + self.file_open_strategy = file_open_strategy; + self + } + + /// Sets the maximum file size in bytes for log rotation. + /// + /// Values larger than [`u64::MAX`] will be clamped to [`u64::MAX`]. + /// In v3, this parameter will be changed to `u64`. + /// + /// Default is `40_000` + pub fn max_file_size(mut self, max_file_size: u128) -> Self { + self.max_file_size = max_file_size.min(u64::MAX as u128); + self + } + + /// Clears the format so that only the message is logged. + /// + /// e.g. `log::info!("message")` will log out `message` + pub fn clear_format(mut self) -> Self { + self.dispatch = self.dispatch.format(|out, message, _record| { + out.finish(format_args!("{message}")); + }); + self + } + + /// Sets the formatter of this dispatch. The closure should accept a + /// callback, a message and a log record, and write the resulting + /// format to the writer. + /// + /// The log record is passed for completeness, but the `args()` method of + /// the record should be ignored, and the [`std::fmt::Arguments`] given + /// should be used instead. `record.args()` may be used to retrieve the + /// _original_ log message, but in order to allow for true log + /// chaining, formatters should use the given message instead whenever + /// including the message in the output. + /// + /// To avoid all allocation of intermediate results, the formatter is + /// "completed" by calling a callback, which then calls the rest of the + /// logging chain with the new formatted message. The callback object keeps + /// track of if it was called or not via a stack boolean as well, so if + /// you don't use `out.finish` the log message will continue down + /// the logger chain unformatted. + /// + /// Example usage: + /// + /// ``` + /// tauri_plugin_log::Builder::new() + /// .format(|out, message, record| { + /// out.finish(format_args!( + /// "[{} {}] {}", + /// record.level(), + /// record.target(), + /// message + /// )) + /// }); + /// ``` + pub fn format(mut self, formatter: F) -> Self + where + F: Fn(FormatCallback, &Arguments, &Record) + Sync + Send + 'static, + { + self.dispatch = self.dispatch.format(formatter); + self + } + + /// Sets the overarching level filter for this logger. + /// All messages not already filtered by something set by [`Self::level_for`] will be affected. + /// + /// All messages filtered will be discarded if less severe than the given level. + /// + /// Default level is [`log::LevelFilter::Trace`]. + pub fn level(mut self, level_filter: impl Into) -> Self { + self.dispatch = self.dispatch.level(level_filter.into()); + self + } + + /// Sets a per-target log level filter. Default target for log messages is + /// `crate_name::module_name` or + /// `crate_name` for logs in the crate root. Targets can also be set with + /// `info!(target: "target-name", ...)`. + /// + /// For each log record fern will first try to match the most specific + /// level_for, and then progressively more general ones until either a + /// matching level is found, or the default level is used. + /// + /// For example, a log for the target `hyper::http::h1` will first test a + /// level_for for `hyper::http::h1`, then for `hyper::http`, then for + /// `hyper`, then use the default level. + /// + /// Examples: + /// + /// A program wants to include a lot of debugging output, but the library + /// "hyper" is known to work well, so debug output from it should be + /// excluded: + /// + /// ``` + /// # fn main() { + /// tauri_plugin_log::Builder::new() + /// .level(log::LevelFilter::Trace) + /// .level_for("hyper", log::LevelFilter::Info) + /// # ; + /// # } + /// ``` + pub fn level_for(mut self, module: impl Into>, level: LevelFilter) -> Self { + self.dispatch = self.dispatch.level_for(module, level); + self + } + + /// Adds a custom filter which can reject messages passing through this logger. + /// + /// [`Self::level`] and [`Self::level_for`] are preferred if applicable. + /// + /// Example usage: + /// + /// ``` + /// tauri_plugin_log::Builder::new() + /// .level(log::LevelFilter::Info) + /// .filter(|metadata| { + /// // Reject messages with the `Error` log level. + /// metadata.level() != log::Level::Error + /// }); + /// ``` + pub fn filter(mut self, filter: F) -> Self + where + F: Fn(&log::Metadata) -> bool + Send + Sync + 'static, + { + self.dispatch = self.dispatch.filter(filter); + self + } + + /// Removes all targets. Useful to ignore the default targets and reconfigure them. + pub fn clear_targets(mut self) -> Self { + self.targets.clear(); + self + } + + /// Adds a log target to the logger. + /// + /// ```rust + /// use tauri_plugin_log::{Target, TargetKind}; + /// tauri_plugin_log::Builder::new() + /// .target(Target::new(TargetKind::Webview)); + /// ``` + /// + /// The default targets are + /// + /// ```rust + /// # use tauri_plugin_log::{Target, TargetKind, Builder}; + /// # Builder::new() + /// # .targets( + /// [ + /// Target::new(TargetKind::Stdout), + /// Target::new(TargetKind::LogDir { file_name: None }), + /// ] + /// # ); + /// ``` + pub fn target(mut self, target: Target) -> Self { + self.targets.push(target); + self + } + + /// Skip the creation and global registration of a logger + /// + /// If you wish to use your own global logger, you must call `skip_logger` so that the plugin does not attempt to set a second global logger. + /// In this configuration, no logger will be created and the plugin's `log` command will rely on the result of `log::logger()`. + /// You will be responsible for configuring the logger yourself and any included targets will be ignored. + /// If ever initializing the plugin multiple times, such as if registering the plugin while testing, call this method to avoid panicking when registering multiple loggers. + /// For interacting with `tracing`, you can leverage the `tracing-log` logger to forward logs to `tracing` or enable the `tracing` feature for this plugin to emit events directly to the tracing system. + /// Both scenarios require calling this method. + /// + /// ```rust + /// # struct SimpleLogger; + /// # impl log::Log for SimpleLogger { + /// # fn enabled(&self, _metadata: &log::Metadata) -> bool { true } + /// # fn log(&self, _record: &log::Record) {} + /// # fn flush(&self) {} + /// # } + /// static LOGGER: SimpleLogger = SimpleLogger; + /// + /// log::set_logger(&LOGGER).expect("a logger was already set"); + /// log::set_max_level(log::LevelFilter::Info); + /// tauri_plugin_log::Builder::new() + /// .skip_logger(); + /// ``` + pub fn skip_logger(mut self) -> Self { + self.is_skip_logger = true; + self + } + + /// Replaces the targets of the logger. + /// + /// ```rust + /// use tauri_plugin_log::{Target, TargetKind, WEBVIEW_TARGET}; + /// tauri_plugin_log::Builder::new() + /// .targets([ + /// Target::new(TargetKind::Webview), + /// Target::new(TargetKind::LogDir { file_name: Some("webview".into()) }).filter(|metadata| metadata.target().starts_with(WEBVIEW_TARGET)), + /// Target::new(TargetKind::LogDir { file_name: Some("rust".into()) }).filter(|metadata| !metadata.target().starts_with(WEBVIEW_TARGET)), + /// ]); + /// ``` + /// + /// The default targets are + /// + /// ```rust + /// # use tauri_plugin_log::{Target, TargetKind, Builder}; + /// # Builder::new() + /// # .targets( + /// [ + /// Target::new(TargetKind::Stdout), + /// Target::new(TargetKind::LogDir { file_name: None }), + /// ] + /// # ); + /// ``` + pub fn targets(mut self, targets: impl IntoIterator) -> Self { + self.targets = Vec::from_iter(targets); + self + } + + #[cfg(feature = "colored")] + pub fn with_colors(self, colors: fern::colors::ColoredLevelConfig) -> Self { + let format = format_description!("[[[year]-[month]-[day]][[[hour]:[minute]:[second]]"); + + let timezone_strategy = self.timezone_strategy.clone(); + self.format(move |out, message, record| { + out.finish(format_args!( + "{}[{}][{}] {}", + timezone_strategy.get_now().format(&format).unwrap(), + colors.color(record.level()), + record.target(), + message + )) + }) + } + + fn acquire_logger( + app_handle: &AppHandle, + mut dispatch: fern::Dispatch, + rotation_strategy: RotationStrategy, + timezone_strategy: TimezoneStrategy, + file_open_strategy: FileOpenStrategy, + max_file_size: u64, + targets: Vec, + ) -> Result<(log::LevelFilter, Box), Error> { + let app_name = &app_handle.package_info().name; + + // setup targets + for target in targets { + let mut target_dispatch = fern::Dispatch::new(); + for filter in target.filters { + target_dispatch = target_dispatch.filter(filter); + } + if let Some(formatter) = target.formatter { + target_dispatch = target_dispatch.format(formatter); + } + + let logger = match target.kind { + #[cfg(target_os = "android")] + TargetKind::Stdout | TargetKind::Stderr => fern::Output::call(android_logger::log), + #[cfg(target_os = "ios")] + TargetKind::Stdout | TargetKind::Stderr => fern::Output::call(move |record| { + let message = format!("{}", record.args()); + unsafe { + ios::tauri_log( + match record.level() { + log::Level::Trace | log::Level::Debug => 1, + log::Level::Info => 2, + log::Level::Warn | log::Level::Error => 3, + }, + // The string is allocated in rust, so we must + // autorelease it rust to give it to the Swift + // runtime. + objc2::rc::Retained::autorelease_ptr( + objc2_foundation::NSString::from_str(message.as_str()), + ) as _, + ); + } + }), + #[cfg(desktop)] + TargetKind::Stdout => std::io::stdout().into(), + #[cfg(desktop)] + TargetKind::Stderr => std::io::stderr().into(), + TargetKind::Folder { path, file_name } => { + if !path.exists() { + fs::create_dir_all(&path)?; + } + + let rotator = RotatingFile::new( + &path, + file_name.unwrap_or(app_name.clone()), + max_file_size, + rotation_strategy.clone(), + timezone_strategy.clone(), + file_open_strategy.clone(), + )?; + fern::Output::writer(Box::new(rotator), "\n") + } + TargetKind::LogDir { file_name } => { + let path = app_handle.path().app_log_dir()?; + if !path.exists() { + fs::create_dir_all(&path)?; + } + + let rotator = RotatingFile::new( + &path, + file_name.unwrap_or(app_name.clone()), + max_file_size, + rotation_strategy.clone(), + timezone_strategy.clone(), + file_open_strategy.clone(), + )?; + fern::Output::writer(Box::new(rotator), "\n") + } + TargetKind::Webview => { + let app_handle = app_handle.clone(); + + fern::Output::call(move |record| { + let payload = RecordPayload { + message: record.args().to_string(), + level: record.level().into(), + }; + let app_handle = app_handle.clone(); + tauri::async_runtime::spawn(async move { + let _ = app_handle.emit("log://log", payload); + }); + }) + } + TargetKind::Dispatch(dispatch) => dispatch.into(), + }; + target_dispatch = target_dispatch.chain(logger); + + dispatch = dispatch.chain(target_dispatch); + } + + Ok(dispatch.into_log()) + } + + fn plugin_builder() -> plugin::Builder { + plugin::Builder::new("log").invoke_handler(tauri::generate_handler![commands::log]) + } + + /// Builds the [`TauriPlugin`] and returns it together with the configured [`log::LevelFilter`] and [`log::Log`] + /// implementation, instead of registering them as the global logger. + /// + /// Use this instead of [`Self::build`] when this logger needs to be combined with another one, e.g. when + /// forwarding logs to `tracing` via `tracing-log`, since only a single global logger can be installed. + /// + /// # Errors + /// + /// Returns [`Error::LoggerNotInitialized`] if [`Self::skip_logger`] was called on this builder. + #[allow(clippy::type_complexity)] + pub fn split( + self, + app_handle: &AppHandle, + ) -> Result<(TauriPlugin, log::LevelFilter, Box), Error> { + if self.is_skip_logger { + return Err(Error::LoggerNotInitialized); + } + let plugin = Self::plugin_builder(); + let (max_level, log) = Self::acquire_logger( + app_handle, + self.dispatch, + self.rotation_strategy, + self.timezone_strategy, + self.file_open_strategy, + self.max_file_size as u64, + self.targets, + )?; + + Ok((plugin.build(), max_level, log)) + } + + /// Builds the [`TauriPlugin`], attaching the configured logger as the global logger for the [`log`] crate on + /// setup, unless [`Self::skip_logger`] was called on this builder. + pub fn build(self) -> TauriPlugin { + Self::plugin_builder() + .setup(move |app_handle, _api| { + if !self.is_skip_logger { + let (max_level, log) = Self::acquire_logger( + app_handle, + self.dispatch, + self.rotation_strategy, + self.timezone_strategy, + self.file_open_strategy, + self.max_file_size as u64, + self.targets, + )?; + attach_logger(max_level, log)?; + } + Ok(()) + }) + .build() + } +} + +/// Attaches the given logger +pub fn attach_logger( + max_level: log::LevelFilter, + log: Box, +) -> Result<(), log::SetLoggerError> { + log::set_boxed_logger(log)?; + log::set_max_level(max_level); + Ok(()) +} diff --git a/plugins/log/tsconfig.json b/plugins/log/tsconfig.json new file mode 100644 index 0000000..4374468 --- /dev/null +++ b/plugins/log/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "rootDir": "guest-js" + }, + "include": ["guest-js/*.ts"] +} diff --git a/plugins/mirrors.txt b/plugins/mirrors.txt new file mode 100644 index 0000000..d346da1 --- /dev/null +++ b/plugins/mirrors.txt @@ -0,0 +1,20 @@ +autostart +cli +clipboard-manager +dialog +fs +global-shortcut +http +log +notification +os +positioner +process +shell +sql +store +stronghold +updater +upload +websocket +window-state diff --git a/plugins/nfc/CHANGELOG.md b/plugins/nfc/CHANGELOG.md new file mode 100644 index 0000000..723981e --- /dev/null +++ b/plugins/nfc/CHANGELOG.md @@ -0,0 +1,116 @@ +# Changelog + +## [2.3.6] + +- [`db9c5998`](https://github.com/tauri-apps/plugins-workspace/commit/db9c5998feff9384f9cbbefcbe0d45937c00a1fc) ([#3531](https://github.com/tauri-apps/plugins-workspace/pull/3531) by [@Legend-Master](https://github.com/tauri-apps/plugins-workspace/../../Legend-Master)) Fix missing `consumer-rules.pro` on Gradle v9. +- [`52672616`](https://github.com/tauri-apps/plugins-workspace/commit/526726162f8ddddf9469797436123704a261df87) ([#3419](https://github.com/tauri-apps/plugins-workspace/pull/3419) by [@pascalauroboa](https://github.com/tauri-apps/plugins-workspace/../../pascalauroboa)) Expect a `NFCTag` as input and return a `ScanResponse` in the `scan` function. + +## \[2.3.5] + +- [`ab037b70`](https://github.com/tauri-apps/plugins-workspace/commit/ab037b70d6fb54f1ad07d5fe21c41bd1997b34dd) ([#3339](https://github.com/tauri-apps/plugins-workspace/pull/3339) by [@crazy-genius](https://github.com/tauri-apps/plugins-workspace/../../crazy-genius)) Fixed syntax error in kotlin files potentially causing issues when trying to write to tags when no prior session existed. + +## \[2.3.4] + +- [`b4348cee`](https://github.com/tauri-apps/plugins-workspace/commit/b4348cee926ee3d1db151bc831cdb9049bee717f) ([#3101](https://github.com/tauri-apps/plugins-workspace/pull/3101) by [@bclarke123](https://github.com/tauri-apps/plugins-workspace/../../bclarke123)) Update return value of `isAvailable` to match TypeScript function signature + +## \[2.3.3] + +- [`93426f85`](https://github.com/tauri-apps/plugins-workspace/commit/93426f85120f49beb9f40222bff45185a32d54a9) Fixed an issue that caused docs.rs builds to fail. No user facing changes. + +## \[2.3.2] + +- [`6c9b61fb`](https://github.com/tauri-apps/plugins-workspace/commit/6c9b61fb658145d13893626112fc489f7458aa17) ([#3039](https://github.com/tauri-apps/plugins-workspace/pull/3039) by [@FabianLars](https://github.com/tauri-apps/plugins-workspace/../../FabianLars)) On Android, updated compileSdk to 36. + +## \[2.3.1] + +- [`fe23a5e0`](https://github.com/tauri-apps/plugins-workspace/commit/fe23a5e01399a6ad61426bf8a94a6bb97227cf88) ([#2885](https://github.com/tauri-apps/plugins-workspace/pull/2885) by [@zaphim12](https://github.com/tauri-apps/plugins-workspace/../../zaphim12)) On iOS, the reader session will now get closed properly on errors, preventing dangling invalid sessions that could prevent subsequent write attempts. + +## \[2.3.0] + +- [`f209b2f2`](https://github.com/tauri-apps/plugins-workspace/commit/f209b2f23cb29133c97ad5961fb46ef794dbe063) ([#2804](https://github.com/tauri-apps/plugins-workspace/pull/2804) by [@renovate](https://github.com/tauri-apps/plugins-workspace/../../renovate)) Updated tauri to 2.6 + +## \[2.2.1] + +- [`f634e524`](https://github.com/tauri-apps/plugins-workspace/commit/f634e5248ebe428f8305a59f74c13fc15147fb8e) This is an "empty" release to update the plugins' source files on crates.io and docs.rs. This should fix docs.rs build failures for projects using tauri plugins as dependencies. + +## \[2.2.0] + +- [`3a79266b`](https://github.com/tauri-apps/plugins-workspace/commit/3a79266b8cf96a55b1ae6339d725567d45a44b1d) ([#2173](https://github.com/tauri-apps/plugins-workspace/pull/2173) by [@FabianLars](https://github.com/tauri-apps/plugins-workspace/../../FabianLars)) Bumped all plugins to `v2.2.0`. From now, the versions for the Rust and JavaScript packages of each plugin will be in sync with each other. + +## \[2.0.1] + +- [`a1a82208`](https://github.com/tauri-apps/plugins-workspace/commit/a1a82208ed4ab87f83310be0dc95428aec9ab241) ([#1873](https://github.com/tauri-apps/plugins-workspace/pull/1873) by [@lucasfernog](https://github.com/tauri-apps/plugins-workspace/../../lucasfernog)) Downgrade MSRV to 1.77.2 to support Windows 7. + +## \[2.0.0] + +- [`e2c4dfb6`](https://github.com/tauri-apps/plugins-workspace/commit/e2c4dfb6af43e5dd8d9ceba232c315f5febd55c1) Update to tauri v2 stable release. + +## \[2.0.0-rc.1] + +- [`e2e97db5`](https://github.com/tauri-apps/plugins-workspace/commit/e2e97db51983267f5be84d4f6f0278d58834d1f5) ([#1701](https://github.com/tauri-apps/plugins-workspace/pull/1701) by [@lucasfernog](https://github.com/tauri-apps/plugins-workspace/../../lucasfernog)) Update to tauri 2.0.0-rc.8 + +## \[2.0.0-rc.2] + +- [`b9147758`](https://github.com/tauri-apps/plugins-workspace/commit/b914775898c2bee7ceb20bd17ee595005cd17a64) ([#1679](https://github.com/tauri-apps/plugins-workspace/pull/1679) by [@lucasfernog](https://github.com/tauri-apps/plugins-workspace/../../lucasfernog)) Explicitly set a minimum macOS version for the Swift package. + +## \[2.0.0-rc.1] + +### changes + +- [`6b079cfd`](https://github.com/tauri-apps/plugins-workspace/commit/6b079cfdd107c94abc2c7300f6af00bac3ff4040) ([#1649](https://github.com/tauri-apps/plugins-workspace/pull/1649) by [@ahqsoftwares](https://github.com/tauri-apps/plugins-workspace/../../ahqsoftwares)) Remove targetSdk from build.kts files as it is deprecated and will be removed from DSL v9.0 + +## \[2.0.0-rc.0] + +- [`9887d1`](https://github.com/tauri-apps/plugins-workspace/commit/9887d14bd0e971c4c0f5c1188fc4005d3fc2e29e) Update to tauri RC. + +## \[2.0.0-beta.8] + +- [`99d6ac0f`](https://github.com/tauri-apps/plugins-workspace/commit/99d6ac0f9506a6a4a1aa59c728157190a7441af6) ([#1606](https://github.com/tauri-apps/plugins-workspace/pull/1606) by [@FabianLars](https://github.com/tauri-apps/plugins-workspace/../../FabianLars)) The JS packages now specify the *minimum* `@tauri-apps/api` version instead of a single exact version. +- [`6de87966`](https://github.com/tauri-apps/plugins-workspace/commit/6de87966ecc00ad9d91c25be452f1f46bd2b7e1f) ([#1597](https://github.com/tauri-apps/plugins-workspace/pull/1597) by [@Legend-Master](https://github.com/tauri-apps/plugins-workspace/../../Legend-Master)) Update to tauri beta.25. + +## \[2.0.0-beta.7] + +- [`22a17980`](https://github.com/tauri-apps/plugins-workspace/commit/22a17980ff4f6f8c40adb1b8f4ffc6dae2fe7e30) ([#1537](https://github.com/tauri-apps/plugins-workspace/pull/1537) by [@lucasfernog](https://github.com/tauri-apps/plugins-workspace/../../lucasfernog)) Update to tauri beta.24. + +## \[2.0.0-beta.6] + +- [`76daee7a`](https://github.com/tauri-apps/plugins-workspace/commit/76daee7aafece34de3092c86e531cf9eb1138989) ([#1512](https://github.com/tauri-apps/plugins-workspace/pull/1512) by [@renovate](https://github.com/tauri-apps/plugins-workspace/../../renovate)) Update to tauri beta.23. + +## \[2.0.0-beta.5] + +- [`9013854f`](https://github.com/tauri-apps/plugins-workspace/commit/9013854f42a49a230b9dbb9d02774765528a923f)([#1382](https://github.com/tauri-apps/plugins-workspace/pull/1382)) Update to tauri beta.22. + +## \[2.0.0-beta.4] + +- [`430bd6f4`](https://github.com/tauri-apps/plugins-workspace/commit/430bd6f4f379bee5d232ae6b098ae131db7f178a)([#1363](https://github.com/tauri-apps/plugins-workspace/pull/1363)) Update to tauri beta.20. + +## \[2.0.0-beta.3] + +- [`bd1ed590`](https://github.com/tauri-apps/plugins-workspace/commit/bd1ed5903ffcce5500310dac1e59e8c67674ef1e)([#1237](https://github.com/tauri-apps/plugins-workspace/pull/1237)) Update to tauri beta.17. + +## \[2.0.0-beta.3] + +- [`a04ea2f`](https://github.com/tauri-apps/plugins-workspace/commit/a04ea2f38294d5a3987578283badc8eec87a7752)([#1071](https://github.com/tauri-apps/plugins-workspace/pull/1071)) The global API script is now only added to the binary when the `withGlobalTauri` config is true. + +## \[2.0.0-beta.2] + +- [`99bea25`](https://github.com/tauri-apps/plugins-workspace/commit/99bea2559c2c0648c2519c50a18cd124dacef57b)([#1005](https://github.com/tauri-apps/plugins-workspace/pull/1005)) Update to tauri beta.8. + +## \[2.0.0-beta.1] + +- [`569defb`](https://github.com/tauri-apps/plugins-workspace/commit/569defbe9492e38938554bb7bdc1be9151456d21) Update to tauri beta.4. + +## \[2.0.0-beta.0] + +- [`d198c01`](https://github.com/tauri-apps/plugins-workspace/commit/d198c014863ee260cb0de88a14b7fc4356ef7474)([#862](https://github.com/tauri-apps/plugins-workspace/pull/862)) Update to tauri beta. +- [`d198c01`](https://github.com/tauri-apps/plugins-workspace/commit/d198c014863ee260cb0de88a14b7fc4356ef7474)([#862](https://github.com/tauri-apps/plugins-workspace/pull/862)) Add permissions. + +## \[2.0.0-alpha.0] + +- [`fe79adb`](https://github.com/tauri-apps/plugins-workspace/commit/fe79adb5c7febd0e912efb5581264d671709fbb0)([#830](https://github.com/tauri-apps/plugins-workspace/pull/830)) Initial release. + commit/fe79adb5c7febd0e912efb5581264d671709fbb0)([#830](https://github.com/tauri-apps/plugins-workspace/pull/830)) Initial release. + 30]\(https://github.com/tauri-apps/plugins-workspace/pull/830)) Initial release. + commit/fe79adb5c7febd0e912efb5581264d671709fbb0)([#830](https://github.com/tauri-apps/plugins-workspace/pull/830)) Initial release. + . + commit/fe79adb5c7febd0e912efb5581264d671709fbb0)([#830](https://github.com/tauri-apps/plugins-workspace/pull/830)) Initial release. + ithub.com/tauri-apps/plugins-workspace/pull/830)) Initial release. diff --git a/plugins/nfc/Cargo.toml b/plugins/nfc/Cargo.toml new file mode 100644 index 0000000..c0e7ed0 --- /dev/null +++ b/plugins/nfc/Cargo.toml @@ -0,0 +1,31 @@ +[package] +name = "tauri-plugin-nfc" +version = "2.3.6" +description = "Read and write NFC tags on Android and iOS." +edition = { workspace = true } +authors = { workspace = true } +license = { workspace = true } +repository = { workspace = true } +links = "tauri-plugin-nfc" +exclude = ["/guest-js", "/package.json", "/rollup.config.js", "/tsconfig.json"] + +[package.metadata.docs.rs] +targets = ["x86_64-linux-android"] + +[package.metadata.platforms.support] +windows = { level = "none", notes = "" } +linux = { level = "none", notes = "" } +macos = { level = "none", notes = "" } +android = { level = "full", notes = "" } +ios = { level = "full", notes = "" } + +[build-dependencies] +tauri-plugin = { workspace = true, features = ["build"] } + +[dependencies] +serde = { workspace = true } +serde_json = { workspace = true } +tauri = { workspace = true } +log = { workspace = true } +thiserror = { workspace = true } +serde_repr = "0.1" diff --git a/plugins/nfc/LICENSE.spdx b/plugins/nfc/LICENSE.spdx new file mode 100644 index 0000000..cdd0df5 --- /dev/null +++ b/plugins/nfc/LICENSE.spdx @@ -0,0 +1,20 @@ +SPDXVersion: SPDX-2.1 +DataLicense: CC0-1.0 +PackageName: tauri +DataFormat: SPDXRef-1 +PackageSupplier: Organization: The Tauri Programme in the Commons Conservancy +PackageHomePage: https://tauri.app +PackageLicenseDeclared: Apache-2.0 +PackageLicenseDeclared: MIT +PackageCopyrightText: 2019-2022, The Tauri Programme in the Commons Conservancy +PackageSummary: Tauri is a rust project that enables developers to make secure +and small desktop applications using a web frontend. + +PackageComment: The package includes the following libraries; see +Relationship information. + +Created: 2019-05-20T09:00:00Z +PackageDownloadLocation: git://github.com/tauri-apps/tauri +PackageDownloadLocation: git+https://github.com/tauri-apps/tauri.git +PackageDownloadLocation: git+ssh://github.com/tauri-apps/tauri.git +Creator: Person: Daniel Thompson-Yvetot \ No newline at end of file diff --git a/plugins/nfc/LICENSE_APACHE-2.0 b/plugins/nfc/LICENSE_APACHE-2.0 new file mode 100644 index 0000000..4947287 --- /dev/null +++ b/plugins/nfc/LICENSE_APACHE-2.0 @@ -0,0 +1,177 @@ + + 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 \ No newline at end of file diff --git a/plugins/nfc/LICENSE_MIT b/plugins/nfc/LICENSE_MIT new file mode 100644 index 0000000..4d75472 --- /dev/null +++ b/plugins/nfc/LICENSE_MIT @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2017 - Present Tauri Apps 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. \ No newline at end of file diff --git a/plugins/nfc/README.md b/plugins/nfc/README.md new file mode 100644 index 0000000..ef1fa6f --- /dev/null +++ b/plugins/nfc/README.md @@ -0,0 +1,112 @@ +![NFC](https://github.com/tauri-apps/plugins-workspace/raw/v2/plugins/nfc/banner.png) + +Read and write NFC tags on Android and iOS. + +| Platform | Supported | +| -------- | --------- | +| Linux | x | +| Windows | x | +| macOS | x | +| Android | ✓ | +| iOS | ✓ | + +## Install + +_This plugin requires a Rust version of at least **1.65**_ + +There are three general methods of installation that we can recommend. + +1. Use crates.io and npm (easiest, and requires you to trust that our publishing pipeline worked) +2. Pull sources directly from Github using git tags / revision hashes (most secure) +3. Git submodule install this repo in your tauri project and then use file protocol to ingest the source (most secure, but inconvenient to use) + +Install the Core plugin by adding the following to your `Cargo.toml` file: + +`src-tauri/Cargo.toml` + +```toml +[dependencies] +tauri-plugin-nfc = "2.0.0" +# alternatively with Git: +tauri-plugin-nfc = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v2" } +``` + +You can install the JavaScript Guest bindings using your preferred JavaScript package manager: + + + +```sh +pnpm add @tauri-apps/plugin-nfc +# or +npm add @tauri-apps/plugin-nfc +# or +yarn add @tauri-apps/plugin-nfc +``` + +## Usage + +First you need to register the core plugin with Tauri: + +`src-tauri/src/lib.rs` + +```rust +fn main() { + tauri::Builder::default() + .plugin(tauri_plugin_nfc::init()) + .run(tauri::generate_context!()) + .expect("error while running tauri application"); +} +``` + +Afterwards all the plugin's APIs are available through the JavaScript guest bindings: + +```javascript +import { scan, textRecord, write } from '@tauri-apps/plugin-nfc' +await scan({ type: 'tag', keepSessionAlive: true }) +await write([textRecord('Tauri is awesome!')]) +``` + +## Contributing + +PRs accepted. Please make sure to read the Contributing Guide before making a pull request. + +## Contributed By + + + + + + + + +
+ + CrabNebula + + + + Impierce + +
+ +## Partners + + + + + + + +
+ + CrabNebula + +
+ +For the complete list of sponsors please visit our [website](https://tauri.app#sponsors) and [Open Collective](https://opencollective.com/tauri). + +## License + +Code: (c) 2015 - Present - The Tauri Programme within The Commons Conservancy. + +MIT or MIT/Apache 2.0 where applicable. diff --git a/plugins/nfc/SECURITY.md b/plugins/nfc/SECURITY.md new file mode 100644 index 0000000..4f09bba --- /dev/null +++ b/plugins/nfc/SECURITY.md @@ -0,0 +1,23 @@ +# Security Policy + +**Do not report security vulnerabilities through public GitHub issues.** + +**Please use the [Private Vulnerability Disclosure](https://docs.github.com/en/code-security/security-advisories/guidance-on-reporting-and-writing-information-about-vulnerabilities/privately-reporting-a-security-vulnerability#privately-reporting-a-security-vulnerability) feature of GitHub.** + +Include as much of the following information: + +- Type of issue (e.g. improper input parsing, privilege escalation, etc.) +- The location of the affected source code (tag/branch/commit or direct URL) +- Any special configuration required to reproduce the issue +- The distribution affected or used to help us with reproduction of the issue +- Step-by-step instructions to reproduce the issue +- Ideally a reproduction repository +- Impact of the issue, including how an attacker might exploit the issue + +We prefer to receive reports in English. + +## Contact + +Please disclose a vulnerability or security relevant issue here: [https://github.com/tauri-apps/plugins-workspace/security/advisories/new](https://github.com/tauri-apps/plugins-workspace/security/advisories/new). + +Alternatively, you can also contact us by email via [security@tauri.app](mailto:security@tauri.app). diff --git a/plugins/nfc/android/.gitignore b/plugins/nfc/android/.gitignore new file mode 100644 index 0000000..c0f21ec --- /dev/null +++ b/plugins/nfc/android/.gitignore @@ -0,0 +1,2 @@ +/build +/.tauri diff --git a/plugins/nfc/android/build.gradle.kts b/plugins/nfc/android/build.gradle.kts new file mode 100644 index 0000000..bf691ef --- /dev/null +++ b/plugins/nfc/android/build.gradle.kts @@ -0,0 +1,36 @@ +plugins { + id("com.android.library") + id("org.jetbrains.kotlin.android") +} + +android { + namespace = "app.tauri.nfc" + compileSdk = 36 + + defaultConfig { + minSdk = 24 + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + consumerProguardFiles("consumer-rules.pro") + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 + } + kotlinOptions { + jvmTarget = "1.8" + } +} + +dependencies { + + implementation("androidx.core:core-ktx:1.9.0") + implementation("androidx.appcompat:appcompat:1.6.0") + implementation("com.google.android.material:material:1.7.0") + implementation("com.fasterxml.jackson.core:jackson-databind:2.15.3") + testImplementation("junit:junit:4.13.2") + androidTestImplementation("androidx.test.ext:junit:1.1.5") + androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1") + implementation(project(":tauri-android")) +} diff --git a/plugins/nfc/android/consumer-rules.pro b/plugins/nfc/android/consumer-rules.pro new file mode 100644 index 0000000..481bb43 --- /dev/null +++ b/plugins/nfc/android/consumer-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/plugins/nfc/android/settings.gradle b/plugins/nfc/android/settings.gradle new file mode 100644 index 0000000..14a752e --- /dev/null +++ b/plugins/nfc/android/settings.gradle @@ -0,0 +1,2 @@ +include ':tauri-android' +project(':tauri-android').projectDir = new File('./.tauri/tauri-api') diff --git a/plugins/nfc/android/src/androidTest/java/ExampleInstrumentedTest.kt b/plugins/nfc/android/src/androidTest/java/ExampleInstrumentedTest.kt new file mode 100644 index 0000000..f0f7b66 --- /dev/null +++ b/plugins/nfc/android/src/androidTest/java/ExampleInstrumentedTest.kt @@ -0,0 +1,28 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +package app.tauri.nfc + +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.ext.junit.runners.AndroidJUnit4 + +import org.junit.Test +import org.junit.runner.RunWith + +import org.junit.Assert.* + +/** + * Instrumented test, which will execute on an Android device. + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +@RunWith(AndroidJUnit4::class) +class ExampleInstrumentedTest { + @Test + fun useAppContext() { + // Context of the app under test. + val appContext = InstrumentationRegistry.getInstrumentation().targetContext + assertEquals("app.tauri.nfc", appContext.packageName) + } +} diff --git a/plugins/nfc/android/src/main/AndroidManifest.xml b/plugins/nfc/android/src/main/AndroidManifest.xml new file mode 100644 index 0000000..7603a35 --- /dev/null +++ b/plugins/nfc/android/src/main/AndroidManifest.xml @@ -0,0 +1,4 @@ + + + + diff --git a/plugins/nfc/android/src/main/java/NfcPlugin.kt b/plugins/nfc/android/src/main/java/NfcPlugin.kt new file mode 100644 index 0000000..04649b9 --- /dev/null +++ b/plugins/nfc/android/src/main/java/NfcPlugin.kt @@ -0,0 +1,519 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +package app.tauri.nfc + +import android.app.Activity +import android.app.PendingIntent +import android.content.Intent +import android.content.IntentFilter +import android.nfc.NdefMessage +import android.nfc.NdefRecord +import android.nfc.NfcAdapter +import android.nfc.Tag +import android.nfc.tech.Ndef +import android.nfc.tech.NdefFormatable +import android.os.Build +import android.os.Parcelable +import android.os.PatternMatcher +import android.webkit.WebView +import app.tauri.Logger +import app.tauri.annotation.Command +import app.tauri.annotation.InvokeArg +import app.tauri.annotation.TauriPlugin +import app.tauri.plugin.Invoke +import app.tauri.plugin.JSArray +import app.tauri.plugin.JSObject +import app.tauri.plugin.Plugin +import com.fasterxml.jackson.annotation.JsonValue +import com.fasterxml.jackson.core.JsonParser +import com.fasterxml.jackson.databind.DeserializationContext +import com.fasterxml.jackson.databind.JsonDeserializer +import com.fasterxml.jackson.databind.JsonNode +import com.fasterxml.jackson.databind.annotation.JsonDeserialize +import org.json.JSONArray +import java.io.IOException +import kotlin.concurrent.thread + +sealed class NfcAction { + object Read : NfcAction() + data class Write(val message: NdefMessage) : NfcAction() +} + +@InvokeArg +class UriFilter { + var scheme: String? = null + var host: String? = null + var pathPrefix: String? = null +} + +@InvokeArg +enum class TechKind(@JsonValue val value: String) { + IsoDep("IsoDep"), + MifareClassic("MifareClassic"), + MifareUltralight("MifareUltralight"), + Ndef("Ndef"), + NdefFormatable("NdefFormatable"), + NfcA("NfcA"), + NfcB("NfcB"), + NfcBarcode("NfcBarcode"), + NfcF("NfcF"), + NfcV("NfcV"); + + fun className(): String { + return when (this) { + IsoDep -> { + android.nfc.tech.IsoDep::class.java.name + } + MifareClassic -> { + android.nfc.tech.MifareClassic::class.java.name + } + MifareUltralight -> { + android.nfc.tech.MifareUltralight::class.java.name + } + Ndef -> { + android.nfc.tech.Ndef::class.java.name + } + NdefFormatable -> { + android.nfc.tech.NdefFormatable::class.java.name + } + NfcA -> { + android.nfc.tech.NfcA::class.java.name + } + NfcB -> { + android.nfc.tech.NfcB::class.java.name + } + NfcBarcode -> { + android.nfc.tech.NfcBarcode::class.java.name + } + NfcF -> { + android.nfc.tech.NfcF::class.java.name + } + NfcV -> { + android.nfc.tech.NfcV::class.java.name + } + } + } +} + +private fun addDataFilters(intentFilter: IntentFilter, uri: UriFilter?, mimeType: String?) { + uri?.let { it -> { + it.scheme?.let { + intentFilter.addDataScheme(it) + } + it.host?.let { + intentFilter.addDataAuthority(it, null) + } + it.pathPrefix?.let { + intentFilter.addDataPath(it, PatternMatcher.PATTERN_PREFIX) + } + }} + mimeType?.let { + intentFilter.addDataType(it) + } +} + +@InvokeArg +@JsonDeserialize(using = ScanKindDeserializer::class) +sealed class ScanKind { + @JsonDeserialize + class Tag: ScanKind() { + var mimeType: String? = null + var uri: UriFilter? = null + } + @JsonDeserialize + class Ndef: ScanKind() { + var mimeType: String? = null + var uri: UriFilter? = null + var techLists: Array>? = null + } + + fun filters(): Array? { + return when (this) { + is Tag -> { + val intentFilter = IntentFilter(NfcAdapter.ACTION_TAG_DISCOVERED) + addDataFilters(intentFilter, uri, mimeType) + arrayOf(intentFilter) + } + is Ndef -> { + val intentFilter = IntentFilter(if (techLists == null) NfcAdapter.ACTION_NDEF_DISCOVERED else NfcAdapter.ACTION_TECH_DISCOVERED) + addDataFilters(intentFilter, uri, mimeType) + arrayOf(intentFilter) + } + } + } + + fun techLists(): Array>? { + return when (this) { + is Tag -> null + is Ndef -> { + techLists?.let { + val techs = mutableListOf>() + for (techList in it) { + val list = mutableListOf() + for (tech in techList) { + list.add(tech.className()) + } + techs.add(list.toTypedArray()) + } + techs.toTypedArray() + } ?: run { + null + } + } + } + } +} + +internal class ScanKindDeserializer: JsonDeserializer() { + override fun deserialize( + jsonParser: JsonParser, + deserializationContext: DeserializationContext + ): ScanKind { + val node: JsonNode = jsonParser.codec.readTree(jsonParser) + node.get("tag")?.let { + return jsonParser.codec.treeToValue(it, ScanKind.Tag::class.java) + } ?: node.get("ndef")?.let { + return jsonParser.codec.treeToValue(it, ScanKind.Ndef::class.java) + } ?: run { + throw Error("unknown scan kind $node") + } + } +} + +@InvokeArg +class ScanOptions { + lateinit var kind: ScanKind + var keepSessionAlive: Boolean = false +} + +@InvokeArg +class NDEFRecordData { + var format: Short = 0 + var kind: ByteArray = ByteArray(0) + var id: ByteArray = ByteArray(0) + var payload: ByteArray = ByteArray(0) +} + +@InvokeArg +class WriteOptions { + var kind: ScanKind? = null + lateinit var records: Array +} + +class Session( + val action: NfcAction, + val invoke: Invoke, + val keepAlive: Boolean, + var tag: Tag? = null, + val filters: Array? = null, + val techLists: Array>? = null +) + +@TauriPlugin +class NfcPlugin(private val activity: Activity) : Plugin(activity) { + private lateinit var webView: WebView + + private var nfcAdapter: NfcAdapter? = null + private var session: Session? = null + + override fun load(webView: WebView) { + super.load(webView) + this.webView = webView + this.nfcAdapter = NfcAdapter.getDefaultAdapter(activity.applicationContext) + } + + override fun onNewIntent(intent: Intent) { + Logger.info("NFC", "onNewIntent") + super.onNewIntent(intent) + + val extraTag = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + intent.getParcelableExtra(NfcAdapter.EXTRA_TAG, Tag::class.java) + } else { + @Suppress("DEPRECATION") + intent.getParcelableExtra(NfcAdapter.EXTRA_TAG) + } + + extraTag?.let { tag -> + session?.let { + if (it.keepAlive) { + it.tag = tag + } + } + + when (session?.action) { + is NfcAction.Read -> readTag(tag, intent) + is NfcAction.Write -> thread { + if (session?.action is NfcAction.Write) { + try { + writeTag(tag, (session?.action as NfcAction.Write).message) + session?.invoke?.resolve() + } catch (e: Exception) { + session?.invoke?.reject(e.toString()) + } finally { + if (this.session?.keepAlive != true) { + this.session = null + disableNFCInForeground() + } + } + } + } + + else -> {} + } + } + + } + + override fun onPause() { + disableNFCInForeground() + super.onPause() + Logger.info("NFC", "onPause") + } + + override fun onResume() { + super.onResume() + Logger.info("NFC", "onResume") + session?.let { + enableNFCInForeground(it.filters, it.techLists) + } + } + + private fun isAvailable(): Pair { + val available: Boolean + var errorReason: String? = null + + if (this.nfcAdapter === null) { + available = false + errorReason = "Device does not have NFC capabilities" + } else if (this.nfcAdapter?.isEnabled == false) { + available = false + errorReason = "NFC is disabled in device settings" + } else { + available = true + } + + return Pair(available, errorReason) + } + + @Command + fun isAvailable(invoke: Invoke) { + val ret = JSObject() + ret.put("available", isAvailable().first) + invoke.resolve(ret) + } + + @Command + fun scan(invoke: Invoke) { + val status = isAvailable() + if (!status.first) { + invoke.reject("NFC unavailable: " + status.second) + return + } + + val args = invoke.parseArgs(ScanOptions::class.java) + + val filters = args.kind.filters() + val techLists = args.kind.techLists() + enableNFCInForeground(filters, techLists) + + session = Session(NfcAction.Read, invoke, args.keepSessionAlive, null, filters, techLists) + } + + @Command + fun write(invoke: Invoke) { + val status = isAvailable() + if (!status.first) { + invoke.reject("NFC unavailable: " + status.second) + return + } + + val args = invoke.parseArgs(WriteOptions::class.java) + + val ndefRecords: MutableList = ArrayList() + for (record in args.records) { + ndefRecords.add(NdefRecord(record.format, record.kind, record.id, record.payload)) + } + + val message = NdefMessage(ndefRecords.toTypedArray()) + + session?.let { session -> + session.tag?.let { + try { + writeTag(it, message) + invoke.resolve() + } catch (e: Exception) { + invoke.reject(e.toString()) + } finally { + if (this.session?.keepAlive != true) { + this.session = null + disableNFCInForeground() + } + } + } ?: run { + invoke.reject("connected tag not found, please wait for it to be available and then call write()") + } + } ?: run { + args.kind?.let { kind -> + val filters = kind.filters() + val techLists = kind.techLists() + enableNFCInForeground(filters, techLists) + session = Session(NfcAction.Write(message), invoke, true, null, filters, techLists) + Logger.warn("NFC", "Write Mode Enabled") + } ?: run { + invoke.reject("Missing `kind` for write") + } + + } + } + + private fun readTag(tag: Tag, intent: Intent) { + try { + val rawMessages = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + intent.getParcelableArrayExtra(NfcAdapter.EXTRA_NDEF_MESSAGES, Parcelable::class.java) + } else { + @Suppress("DEPRECATION") + intent.getParcelableArrayExtra(NfcAdapter.EXTRA_NDEF_MESSAGES) + } + + when (intent.action) { + NfcAdapter.ACTION_NDEF_DISCOVERED -> { + // For some reason this one never triggers. + Logger.info("NFC", "new NDEF intent") + readTagInner(tag, rawMessages) + } + NfcAdapter.ACTION_TECH_DISCOVERED -> { + // For some reason this always triggers instead of NDEF_DISCOVERED even though we set ndef filters right now + Logger.info("NFC", "new TECH intent") + // TODO: handle different techs. Don't assume ndef. + readTagInner(tag, rawMessages) + } + NfcAdapter.ACTION_TAG_DISCOVERED -> { + // This should never trigger when an app handles NDEF and TECH + // TODO: Don't assume ndef. + readTagInner(tag, rawMessages) + } + } + } catch (e: Exception) { + session?.invoke?.reject("failed to read tag", e) + } finally { + if (this.session?.keepAlive != true) { + this.session = null + } + // TODO this crashes? disableNFCInForeground() + } + } + + private fun readTagInner(tag: Tag?, rawMessages: Array?) { + val ndefMessage = rawMessages?.get(0) as NdefMessage? + + val records = ndefMessage?.records ?: arrayOf() + + val jsonRecords = Array(records.size) { i -> recordToJson(records[i]) } + + val ret = JSObject() + if (tag !== null) { + ret.put("id", fromU8Array(tag.id)) + // TODO There's also ndef.type which returns the ndef spec type which may be interesting to know too? + ret.put("kind", JSArray.from(tag.techList)) + } + ret.put("records", JSArray.from(jsonRecords)) + + session?.invoke?.resolve(ret) + } + + private fun writeTag(tag: Tag, message: NdefMessage) { + // This should return tags that are already in ndef format + val ndefTag = Ndef.get(tag) + if (ndefTag !== null) { + // We have to connect first to check maxSize. + try { + ndefTag.connect() + } catch (e: IOException) { + throw Exception("Couldn't connect to NFC tag", e) + } + + if (ndefTag.maxSize < message.toByteArray().size) { + throw Exception("The message is too large for the provided NFC tag") + } else if (!ndefTag.isWritable) { + throw Exception("NFC tag is read-only") + } else { + try { + ndefTag.writeNdefMessage(message) + } catch (e: Exception) { + throw Exception("Couldn't write message to NFC tag", e) + } + } + + try { + ndefTag.close() + } catch (e: IOException) { + Logger.error("failed to close tag", e) + } + + return + } + + // This should cover tags that are not yet in ndef format but can be converted + val ndefFormatableTag = NdefFormatable.get(tag) + if (ndefFormatableTag !== null) { + try { + ndefFormatableTag.connect() + ndefFormatableTag.format(message) + } catch (e: Exception) { + throw Exception("Couldn't format tag as Ndef", e) + } + + try { + ndefFormatableTag.close() + } catch (e: IOException) { + Logger.error("failed to close tag", e) + } + + return + } + + // if we get to this line, the tag was neither Ndef nor NdefFormatable compatible + throw Exception("Tag doesn't support Ndef format") + } + + // TODO: Use ReaderMode instead of ForegroundDispatch + private fun enableNFCInForeground(filters: Array?, techLists: Array>?) { + val flag = + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_MUTABLE else PendingIntent.FLAG_UPDATE_CURRENT + val pendingIntent = PendingIntent.getActivity( + activity, 0, + Intent( + activity, + activity.javaClass + ).addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP), + flag + ) + + nfcAdapter?.enableForegroundDispatch(activity, pendingIntent, filters, techLists) + } + + private fun disableNFCInForeground() { + activity.runOnUiThread { + nfcAdapter?.disableForegroundDispatch(activity) + } + } +} + +private fun fromU8Array(byteArray: ByteArray): JSONArray { + val json = JSONArray() + for (byte in byteArray) { + json.put(byte) + } + return json +} + +private fun recordToJson(record: NdefRecord): JSObject { + val json = JSObject() + json.put("tnf", record.tnf) + json.put("kind", fromU8Array(record.type)) + json.put("id", fromU8Array(record.id)) + json.put("payload", fromU8Array(record.payload)) + return json +} \ No newline at end of file diff --git a/plugins/nfc/android/src/main/res/xml/nfc_tech_filter.xml b/plugins/nfc/android/src/main/res/xml/nfc_tech_filter.xml new file mode 100644 index 0000000..994905a --- /dev/null +++ b/plugins/nfc/android/src/main/res/xml/nfc_tech_filter.xml @@ -0,0 +1,13 @@ + + + android.nfc.tech.IsoDep + android.nfc.tech.NfcA + android.nfc.tech.NfcB + android.nfc.tech.NfcF + android.nfc.tech.NfcV + android.nfc.tech.Ndef + android.nfc.tech.NdefFormatable + android.nfc.tech.MifareClassic + android.nfc.tech.MifareUltralight + + diff --git a/plugins/nfc/android/src/test/java/ExampleUnitTest.kt b/plugins/nfc/android/src/test/java/ExampleUnitTest.kt new file mode 100644 index 0000000..2af426f --- /dev/null +++ b/plugins/nfc/android/src/test/java/ExampleUnitTest.kt @@ -0,0 +1,21 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +package app.tauri.nfc + +import org.junit.Test + +import org.junit.Assert.* + +/** + * Example local unit test, which will execute on the development machine (host). + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +class ExampleUnitTest { + @Test + fun addition_isCorrect() { + assertEquals(4, 2 + 2) + } +} diff --git a/plugins/nfc/api-iife.js b/plugins/nfc/api-iife.js new file mode 100644 index 0000000..05c5f06 --- /dev/null +++ b/plugins/nfc/api-iife.js @@ -0,0 +1 @@ +if("__TAURI__"in window){var __TAURI_PLUGIN_NFC__=function(n,e){"use strict";const t=[84],r=[85];var o,c;function a(n,e,t,r){return{format:n,kind:"string"==typeof e?Array.from((new TextEncoder).encode(e)):e,id:"string"==typeof t?Array.from((new TextEncoder).encode(t)):t,payload:"string"==typeof r?Array.from((new TextEncoder).encode(r)):r}}n.TechKind=void 0,(o=n.TechKind||(n.TechKind={}))[o.IsoDep=0]="IsoDep",o[o.MifareClassic=1]="MifareClassic",o[o.MifareUltralight=2]="MifareUltralight",o[o.Ndef=3]="Ndef",o[o.NdefFormatable=4]="NdefFormatable",o[o.NfcA=5]="NfcA",o[o.NfcB=6]="NfcB",o[o.NfcBarcode=7]="NfcBarcode",o[o.NfcF=8]="NfcF",o[o.NfcV=9]="NfcV",n.NFCTypeNameFormat=void 0,(c=n.NFCTypeNameFormat||(n.NFCTypeNameFormat={}))[c.Empty=0]="Empty",c[c.NfcWellKnown=1]="NfcWellKnown",c[c.Media=2]="Media",c[c.AbsoluteURI=3]="AbsoluteURI",c[c.NfcExternal=4]="NfcExternal",c[c.Unknown=5]="Unknown",c[c.Unchanged=6]="Unchanged";const i=["","http://www.","https://www.","http://","https://","tel:","mailto:","ftp://anonymous:anonymous@","ftp://ftp.","ftps://","sftp://","smb://","nfs://","ftp://","dav://","news:","telnet://","imap:","rtsp://","urn:","pop:","sip:","sips:","tftp:","btspp://","btl2cap://","btgoep://","tcpobex://","irdaobex://","file://","urn:epc:id:","urn:epc:tag:","urn:epc:pat:","urn:epc:raw:","urn:epc:","urn:nfc:"];function f(n){const{type:e,...t}=n;return{[e]:t}}return n.RTD_TEXT=t,n.RTD_URI=r,n.isAvailable=async function(){const{available:n}=await e.invoke("plugin:nfc|is_available");return n},n.record=a,n.scan=async function(n,t){return await e.invoke("plugin:nfc|scan",{kind:f(n),...t})},n.textRecord=function(e,r,o="en"){const c=Array.from((new TextEncoder).encode(o+e));return c.unshift(o.length),a(n.NFCTypeNameFormat.NfcWellKnown,t,r??[],c)},n.uriRecord=function(e,t){return a(n.NFCTypeNameFormat.NfcWellKnown,r,t??[],function(n){let e="";i.slice(1).forEach(function(t){0!==e.length&&"urn:"!==e||0!==n.indexOf(t)||(e=t)}),0===e.length&&(e="");const t=Array.from((new TextEncoder).encode(n.slice(e.length))),r=i.indexOf(e);return t.unshift(r),t}(e))},n.write=async function(n,t){const{kind:r,...o}=t??{};r&&(o.kind=f(r)),await e.invoke("plugin:nfc|write",{records:n,...o})},n}({},window.__TAURI__.core);Object.defineProperty(window.__TAURI__,"nfc",{value:__TAURI_PLUGIN_NFC__})} diff --git a/plugins/nfc/build.rs b/plugins/nfc/build.rs new file mode 100644 index 0000000..bdcd84b --- /dev/null +++ b/plugins/nfc/build.rs @@ -0,0 +1,45 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +const COMMANDS: &[&str] = &["is_available", "write", "scan"]; + +fn main() { + let result = tauri_plugin::Builder::new(COMMANDS) + .global_api_script_path("./api-iife.js") + .android_path("android") + .ios_path("ios") + .try_build(); + + // when building documentation for Android the plugin build result is always Err() and is irrelevant to the crate documentation build + if !(cfg!(docsrs) && std::env::var("TARGET").unwrap().contains("android")) { + result.unwrap(); + } + + // TODO: triple check if this can reference the plugin's xml as it expects rn + // TODO: This has to be configurable if we want to support handling nfc tags when the app is not open. + tauri_plugin::mobile::update_android_manifest( + "NFC PLUGIN", + "activity", + r#" + + + + + + + + + + + + + + +"# + .to_string(), + ) + .expect("failed to rewrite AndroidManifest.xml"); +} diff --git a/plugins/nfc/contributors/crabnebula.svg b/plugins/nfc/contributors/crabnebula.svg new file mode 100644 index 0000000..a9bb460 --- /dev/null +++ b/plugins/nfc/contributors/crabnebula.svg @@ -0,0 +1,31 @@ + \ No newline at end of file diff --git a/plugins/nfc/contributors/impierce.svg b/plugins/nfc/contributors/impierce.svg new file mode 100644 index 0000000..9d2a510 --- /dev/null +++ b/plugins/nfc/contributors/impierce.svg @@ -0,0 +1,21 @@ + \ No newline at end of file diff --git a/plugins/nfc/guest-js/index.ts b/plugins/nfc/guest-js/index.ts new file mode 100644 index 0000000..35489b9 --- /dev/null +++ b/plugins/nfc/guest-js/index.ts @@ -0,0 +1,469 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +/** + * Read and write NFC tags on Android and iOS. + * + * This plugin is mobile only, the APIs reject on desktop platforms. + * + * @module + */ + +import { invoke } from '@tauri-apps/api/core' + +/** + * Record Type Definition (RTD) of an NDEF well known text record, the `"T"` (`0x54`) byte. + * + * @since 2.0.0 + */ +export const RTD_TEXT = [0x54] // "T" +/** + * Record Type Definition (RTD) of an NDEF well known URI record, the `"U"` (`0x55`) byte. + * + * @since 2.0.0 + */ +export const RTD_URI = [0x55] // "U" + +/** + * Filters the tags to scan by the URI of their payload. + * + * Every property is optional and only the ones that are set take part in the filter. + * **Android only**, the iOS implementation ignores this filter. + */ +export interface UriFilter { + /** Only match URIs with this scheme, e.g. `https`. */ + scheme?: string + /** Only match URIs with this authority (host), e.g. `tauri.app`. */ + host?: string + /** Only match URIs whose path starts with this prefix, e.g. `/docs`. */ + pathPrefix?: string +} + +/** + * The NFC technologies a tag can support, mirroring the `android.nfc.tech` classes. + * + * **Android only**, used by the `techLists` filter of an `ndef` {@link ScanKind}. + */ +export enum TechKind { + /** ISO-DEP (ISO 14443-4) properties and I/O operations. */ + IsoDep, + /** MIFARE Classic properties and I/O operations. */ + MifareClassic, + /** MIFARE Ultralight and MIFARE Ultralight C properties and I/O operations. */ + MifareUltralight, + /** NDEF data and operations on tags that are already formatted as NDEF. */ + Ndef, + /** Formatting operations on tags that can be formatted as NDEF but are not yet. */ + NdefFormatable, + /** NFC-A (ISO 14443-3A) properties and I/O operations. */ + NfcA, + /** NFC-B (ISO 14443-3B) properties and I/O operations. */ + NfcB, + /** NFC Barcode (Kovio NFC Barcode) properties and I/O operations. */ + NfcBarcode, + /** NFC-F (JIS 6319-4) properties and I/O operations. */ + NfcF, + /** NFC-V (ISO 15693) properties and I/O operations. */ + NfcV +} + +/** + * The kind of scan to perform, which defines which tags are matched. + * + * Use `tag` to match any discovered tag and `ndef` to only match tags carrying an NDEF message. + */ +export type ScanKind = + | { + /** Matches any tag that is discovered, whether it carries an NDEF message or not. */ + type: 'tag' + /** Only match tags whose payload URI matches this filter. **Android only**. */ + uri?: UriFilter + /** Only match tags whose payload has this MIME type, e.g. `text/plain`. **Android only**. */ + mimeType?: string + } + | { + /** Only matches tags that carry an NDEF message. */ + type: 'ndef' + /** Only match tags whose NDEF payload URI matches this filter. **Android only**. */ + uri?: UriFilter + /** + * Only match tags whose NDEF payload has this MIME type, e.g. `text/plain`. + * **Android only**. + */ + mimeType?: string + /** + * Each of the tech-lists is considered independently and the activity is considered a match if + * any single tech-list matches the tag that was discovered. + * This provides AND and OR semantics for filtering desired techs. + * + * See for more information. + * + * Examples + * + * ```ts + * import type { TechKind } from "@tauri-apps/plugin-nfc" + * + * const techLists = [ + * // capture anything using NfcF + * [TechKind.NfcF], + * // capture all MIFARE Classics with NDEF payloads + * [TechKind.NfcA, TechKind.MifareClassic, TechKind.Ndef] + * ] + * ``` + */ + techLists?: TechKind[][] + } + +/** Options for the {@link scan} function. */ +export interface ScanOptions { + /** + * Whether the connection to the scanned tag must be kept open after the scan resolves, + * so that a following {@link write} call writes to that tag instead of scanning a new one. + * Defaults to `false`. + */ + keepSessionAlive?: boolean + /** Message displayed in the UI. iOS only. */ + message?: string + /** Message displayed in the UI when the message has been read. iOS only. */ + successMessage?: string +} + +/** Options for the {@link write} function. */ +export interface WriteOptions { + /** + * The kind of scan to perform to find the tag to write to. + * Required on Android unless a {@link scan} session is kept alive. + */ + kind?: ScanKind + /** Message displayed in the UI when reading the tag. iOS only. */ + message?: string + /** Message displayed in the UI when the tag has been read. iOS only. */ + successfulReadMessage?: string + /** Message displayed in the UI when the message has been written. iOS only. */ + successMessage?: string +} + +/** + * The Type Name Format (TNF) of an NDEF record, + * which defines how the record type is interpreted. + */ +export enum NFCTypeNameFormat { + /** The record is empty: type, identifier and payload must be empty. */ + Empty = 0, + /** + * The record type is an NFC Forum well known type, defined by a Record Type Definition (RTD) + * such as {@link RTD_TEXT} or {@link RTD_URI}. + */ + NfcWellKnown = 1, + /** The record type is a MIME media type as defined in RFC 2046, e.g. `text/plain`. */ + Media = 2, + /** The record type is an absolute URI as defined in RFC 3986. */ + AbsoluteURI = 3, + /** The record type is an NFC Forum external type, i.e. a type namespaced by its issuer. */ + NfcExternal = 4, + /** + * The record type is unknown: the type must be empty and the payload interpretation + * is left to the application. + */ + Unknown = 5, + /** + * The record is a middle or last chunk of a chunked record and inherits the type of the + * first chunk, so its own type must be empty. + */ + Unchanged = 6 +} + +/** An NDEF record read from a scanned tag. */ +export interface TagRecord { + /** + * The Type Name Format (TNF) of the record, + * which defines how {@link TagRecord.kind} is interpreted. + */ + tnf: NFCTypeNameFormat + /** The record type bytes. */ + kind: number[] + /** The record identifier bytes. Can be empty. */ + id: number[] + /** The record payload bytes. */ + payload: number[] +} + +/** An NFC tag that has been scanned. */ +export interface Tag { + /** The tag identifier bytes, as reported by the operating system. */ + id: number[] + /** The technologies the tag supports, e.g. `["android.nfc.tech.Ndef"]` on Android. */ + kind: string[] + /** The NDEF records stored on the tag. Empty when the tag holds no NDEF message. */ + records: TagRecord[] +} + +/** + * An NDEF record to be written to a tag. + * + * Use {@link record}, {@link textRecord} or {@link uriRecord} to create one. + */ +export interface NFCRecord { + /** The Type Name Format (TNF) of the record. */ + format: NFCTypeNameFormat + /** + * The record type, interpreted according to {@link NFCRecord.format}. + * For well known records this is a Record Type Definition (RTD) value + * such as {@link RTD_TEXT} or {@link RTD_URI}. + */ + kind: number[] + /** The record identifier. Can be empty. */ + id: number[] + /** The record payload bytes. */ + payload: number[] +} + +/** + * Creates an NDEF record with the given type name format, type, identifier and payload. + * + * Strings are encoded as UTF-8 byte arrays. + * + * @example + * ```typescript + * import { record, NFCTypeNameFormat, write } from '@tauri-apps/plugin-nfc'; + * const mimeRecord = record(NFCTypeNameFormat.Media, 'text/plain', '', 'hello world'); + * await write([mimeRecord], { kind: { type: 'ndef' } }); + * ``` + * + * @param format The Type Name Format (TNF) of the record. + * @param kind The record type, interpreted according to `format`. + * @param id The record identifier. Use an empty string or array when the record has none. + * @param payload The record payload. + * @returns The NDEF record, ready to be written with {@link write}. + * + * @since 2.0.0 + */ +export function record( + format: NFCTypeNameFormat, + kind: string | number[], + id: string | number[], + payload: string | number[] +): NFCRecord { + return { + format, + kind: + typeof kind === 'string' + ? Array.from(new TextEncoder().encode(kind)) + : kind, + id: typeof id === 'string' ? Array.from(new TextEncoder().encode(id)) : id, + payload: + typeof payload === 'string' + ? Array.from(new TextEncoder().encode(payload)) + : payload + } +} + +/** + * Creates an NDEF well known text record ({@link RTD_TEXT}). + * + * The payload is the UTF-8 encoded text prefixed by the language code + * and by a status byte holding the length of that language code. + * + * @example + * ```typescript + * import { textRecord, write } from '@tauri-apps/plugin-nfc'; + * await write([textRecord('hello world')], { kind: { type: 'ndef' } }); + * ``` + * + * @param text The text to store in the record. + * @param id The record identifier. Defaults to an empty identifier. + * @param language The IANA language code of the text. Defaults to `en`. + * @returns The NDEF record, ready to be written with {@link write}. + * + * @since 2.0.0 + */ +export function textRecord( + text: string, + id?: string | number[], + language: string = 'en' +): NFCRecord { + const payload = Array.from(new TextEncoder().encode(language + text)) + payload.unshift(language.length) + return record(NFCTypeNameFormat.NfcWellKnown, RTD_TEXT, id ?? [], payload) +} + +const protocols = [ + '', + 'http://www.', + 'https://www.', + 'http://', + 'https://', + 'tel:', + 'mailto:', + 'ftp://anonymous:anonymous@', + 'ftp://ftp.', + 'ftps://', + 'sftp://', + 'smb://', + 'nfs://', + 'ftp://', + 'dav://', + 'news:', + 'telnet://', + 'imap:', + 'rtsp://', + 'urn:', + 'pop:', + 'sip:', + 'sips:', + 'tftp:', + 'btspp://', + 'btl2cap://', + 'btgoep://', + 'tcpobex://', + 'irdaobex://', + 'file://', + 'urn:epc:id:', + 'urn:epc:tag:', + 'urn:epc:pat:', + 'urn:epc:raw:', + 'urn:epc:', + 'urn:nfc:' +] + +function encodeURI(uri: string): number[] { + let prefix = '' + + protocols.slice(1).forEach(function (protocol) { + if ( + (prefix.length === 0 || prefix === 'urn:') + && uri.indexOf(protocol) === 0 + ) { + prefix = protocol + } + }) + + if (prefix.length === 0) { + prefix = '' + } + + const encoded = Array.from(new TextEncoder().encode(uri.slice(prefix.length))) + const protocolCode = protocols.indexOf(prefix) + // prepend protocol code + encoded.unshift(protocolCode) + + return encoded +} + +/** + * Creates an NDEF well known URI record ({@link RTD_URI}). + * + * The URI is encoded with the NDEF URI abbreviation scheme: a known prefix such as + * `https://` is replaced by the identifier code byte that starts the payload. + * + * @example + * ```typescript + * import { uriRecord, write } from '@tauri-apps/plugin-nfc'; + * await write([uriRecord('https://tauri.app')], { kind: { type: 'ndef' } }); + * ``` + * + * @param uri The URI to store in the record. + * @param id The record identifier. Defaults to an empty identifier. + * @returns The NDEF record, ready to be written with {@link write}. + * + * @since 2.0.0 + */ +export function uriRecord(uri: string, id?: string | number[]): NFCRecord { + return record( + NFCTypeNameFormat.NfcWellKnown, + RTD_URI, + id ?? [], + encodeURI(uri) + ) +} + +function mapScanKind(kind: ScanKind): Record { + const { type: scanKind, ...kindOptions } = kind + return { [scanKind]: kindOptions } +} + +/** + * Scans an NFC tag, resolving when a tag matching the given filters is read. + * + * See for more information. + * + * @example + * ```typescript + * import { scan } from '@tauri-apps/plugin-nfc'; + * const tag = await scan({ type: 'tag' }); + * ``` + * + * @param kind The kind of scan to perform, which defines how tags are matched. + * @param options Additional scan options such as the iOS UI messages and whether the + * session must be kept alive for a following {@link write} call. + * @returns A promise resolving to the tag that has been scanned. + * + * @since 2.0.0 + */ +export async function scan( + kind: ScanKind, + options?: ScanOptions +): Promise { + return await invoke('plugin:nfc|scan', { + kind: mapScanKind(kind), + ...options + }) +} + +/** + * Write to an NFC tag. + * + * If you did not previously call {@link scan} with {@link ScanOptions.keepSessionAlive} set to true, + * it will first scan the tag then write to it. + * + * @example + * ```typescript + * import { uriRecord, write } from '@tauri-apps/plugin-nfc'; + * await write([uriRecord('https://tauri.app')], { kind: { type: 'ndef' } }); + * ``` + * + * @param records The NDEF records to write to the tag. + * @param options Additional write options such as the kind of scan used to find the tag + * and the iOS UI messages. + * + * @since 2.0.0 + */ +export async function write( + records: NFCRecord[], + options?: WriteOptions +): Promise { + const { kind, ...opts } = options ?? {} + if (kind) { + // @ts-expect-error map the property + opts.kind = mapScanKind(kind) + } + await invoke('plugin:nfc|write', { + records, + ...opts + }) +} + +/** + * Checks whether NFC is supported by the device and currently usable by the app. + * + * Resolves to `false` on Android when the device has no NFC adapter or NFC is disabled in the + * device settings, and on iOS when the `NFCReaderUsageDescription` entry is missing from the + * `Info.plist` file or NFC tag reading is unavailable. + * + * @example + * ```typescript + * import { isAvailable } from '@tauri-apps/plugin-nfc'; + * const canScan = await isAvailable(); + * ``` + * + * @returns A promise resolving to whether NFC is available on the device. + * + * @since 2.0.0 + */ +export async function isAvailable(): Promise { + const { available }: { available: boolean } = await invoke( + 'plugin:nfc|is_available' + ) + return available +} diff --git a/plugins/nfc/ios/.gitignore b/plugins/nfc/ios/.gitignore new file mode 100644 index 0000000..5922fda --- /dev/null +++ b/plugins/nfc/ios/.gitignore @@ -0,0 +1,10 @@ +.DS_Store +/.build +/Packages +/*.xcodeproj +xcuserdata/ +DerivedData/ +.swiftpm/config/registries.json +.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata +.netrc +Package.resolved diff --git a/plugins/nfc/ios/Package.swift b/plugins/nfc/ios/Package.swift new file mode 100644 index 0000000..a028db7 --- /dev/null +++ b/plugins/nfc/ios/Package.swift @@ -0,0 +1,34 @@ +// swift-tools-version:5.3 +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +import PackageDescription + +let package = Package( + name: "tauri-plugin-nfc", + platforms: [ + .macOS(.v10_13), + .iOS(.v13), + ], + products: [ + // Products define the executables and libraries a package produces, and make them visible to other packages. + .library( + name: "tauri-plugin-nfc", + type: .static, + targets: ["tauri-plugin-nfc"]) + ], + dependencies: [ + .package(name: "Tauri", path: "../.tauri/tauri-api") + ], + targets: [ + // Targets are the basic building blocks of a package. A target can define a module or a test suite. + // Targets can depend on other targets in this package, and on products in packages this package depends on. + .target( + name: "tauri-plugin-nfc", + dependencies: [ + .byName(name: "Tauri") + ], + path: "Sources") + ] +) diff --git a/plugins/nfc/ios/README.md b/plugins/nfc/ios/README.md new file mode 100644 index 0000000..88a429b --- /dev/null +++ b/plugins/nfc/ios/README.md @@ -0,0 +1,3 @@ +# Tauri Plugin Nfc + +A description of this package. diff --git a/plugins/nfc/ios/Sources/NfcPlugin.swift b/plugins/nfc/ios/Sources/NfcPlugin.swift new file mode 100644 index 0000000..af1a68d --- /dev/null +++ b/plugins/nfc/ios/Sources/NfcPlugin.swift @@ -0,0 +1,523 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +// https://developer.apple.com/documentation/corenfc/building_an_nfc_tag-reader_app + +import CoreNFC +import SwiftRs +import Tauri +import UIKit +import WebKit + +enum ScanKind: Decodable { + case ndef, tag +} + +struct ScanOptions: Decodable { + let kind: ScanKind + var keepSessionAlive: Bool? + var message: String? + var successMessage: String? +} + +struct NDEFRecord: Decodable { + var format: UInt8? + var kind: [UInt8]? + var identifier: [UInt8]? + var payload: [UInt8]? +} + +struct WriteOptions: Decodable { + var kind: ScanKind? + let records: [NDEFRecord] + var message: String? + var successMessage: String? + var successfulReadMessage: String? +} + +enum TagProcessMode { + case write(message: NFCNDEFMessage) + case read +} + +class Session { + let nfcSession: NFCReaderSession? + let invoke: Invoke + var keepAlive: Bool + let tagProcessMode: TagProcessMode + var tagStatus: NFCNDEFStatus? + var tag: NFCNDEFTag? + let successfulReadMessage: String? + let successfulWriteAlertMessage: String? + + init( + nfcSession: NFCReaderSession?, + invoke: Invoke, + keepAlive: Bool, + tagProcessMode: TagProcessMode, + successfulReadMessage: String?, + successfulWriteAlertMessage: String? + ) { + self.nfcSession = nfcSession + self.invoke = invoke + self.keepAlive = keepAlive + self.tagProcessMode = tagProcessMode + self.successfulReadMessage = successfulReadMessage + self.successfulWriteAlertMessage = successfulWriteAlertMessage + } +} + +class NfcStatus { + let available: Bool + let errorReason: String? + + init(available: Bool, errorReason: String?) { + self.available = available + self.errorReason = errorReason + } +} + +class NfcPlugin: Plugin, NFCTagReaderSessionDelegate, NFCNDEFReaderSessionDelegate { + var session: Session? + var status: NfcStatus! + + public override func load(webview: WKWebView) { + var available = false + var errorReason: String? + + let entry = Bundle.main.infoDictionary?["NFCReaderUsageDescription"] as? String + + if entry == nil || entry?.count == 0 { + errorReason = "missing NFCReaderUsageDescription configuration on the Info.plist file" + } else if !NFCNDEFReaderSession.readingAvailable { + errorReason = + "NFC tag reading unavailable, make sure the Near-Field Communication capability on Xcode is enabled and the device supports NFC tag reading" + } else { + available = true + } + + if let error = errorReason { + Logger.error("\(error)") + } + + self.status = NfcStatus(available: available, errorReason: errorReason) + } + + func tagReaderSessionDidBecomeActive( + _ session: NFCTagReaderSession + ) { + Logger.info("tagReaderSessionDidBecomeActive") + } + + func tagReaderSession(_ session: NFCTagReaderSession, didDetect tags: [NFCTag]) { + let tag = tags.first! + + session.connect( + to: tag, + completionHandler: { [self] (error) in + if let error = error { + self.closeSession(session, error: "cannot connect to tag: \(error)") + + } else { + let ndefTag: NFCNDEFTag + switch tag { + case let .feliCa(tag): + ndefTag = tag as NFCNDEFTag + break + case let .miFare(tag): + ndefTag = tag as NFCNDEFTag + break + case let .iso15693(tag): + ndefTag = tag as NFCNDEFTag + break + case let .iso7816(tag): + ndefTag = tag as NFCNDEFTag + break + default: + return + } + + self.processTag( + session: session, tag: ndefTag, metadata: tagMetadata(tag), + mode: self.session!.tagProcessMode) + } + } + ) + } + + func tagReaderSession(_ session: NFCTagReaderSession, didInvalidateWithError error: Error) { + Logger.error("Tag reader session error \(error)") + self.session?.invoke.reject("session invalidated with error: \(error)") + self.session = nil + } + + func readerSession(_ session: NFCNDEFReaderSession, didDetectNDEFs messages: [NFCNDEFMessage]) { + let message = messages.first! + // TODO: do we really need this hook? + self.session?.invoke.resolve(["records": ndefMessageRecords(message)]) + } + + func readerSession(_ session: NFCNDEFReaderSession, didDetect tags: [NFCNDEFTag]) { + let tag = tags.first! + + session.connect( + to: tag, + completionHandler: { [self] (error) in + if let error = error { + self.closeSession(session, error: "cannot connect to tag: \(error)") + + } else { + var metadata: JsonObject = [:] + if tag.isKind(of: NFCFeliCaTag.self) { + metadata["kind"] = ["FeliCa"] + metadata["id"] = nil + } else if let t = tag as? NFCMiFareTag { + metadata["kind"] = ["MiFare"] + metadata["id"] = byteArrayFromData(t.identifier) + } else if let t = tag as? NFCISO15693Tag { + metadata["kind"] = ["ISO15693"] + metadata["id"] = byteArrayFromData(t.identifier) + } else if let t = tag as? NFCISO7816Tag { + metadata["kind"] = ["ISO7816Compatible"] + metadata["id"] = byteArrayFromData(t.identifier) + } + + self.processTag( + session: session, tag: tag, metadata: metadata, + mode: self.session!.tagProcessMode) + } + } + ) + + } + + func readerSession(_ session: NFCNDEFReaderSession, didInvalidateWithError error: Error) { + if (error as NSError).code + == NFCReaderError.Code.readerSessionInvalidationErrorFirstNDEFTagRead.rawValue + { + // not an error because we're using invalidateAfterFirstRead: true + Logger.debug("readerSessionInvalidationErrorFirstNDEFTagRead") + } else { + Logger.error("NDEF reader session error \(error)") + self.session?.invoke.reject("session invalidated with error: \(error)") + self.session = nil + } + } + + private func tagMetadata(_ tag: NFCTag) -> JsonObject { + var metadata: JsonObject = [:] + + switch tag { + case .feliCa: + metadata["kind"] = ["FeliCa"] + metadata["id"] = [] + break + case let .miFare(tag): + metadata["kind"] = ["MiFare"] + metadata["id"] = byteArrayFromData(tag.identifier) + break + case let .iso15693(tag): + metadata["kind"] = ["ISO15693"] + metadata["id"] = byteArrayFromData(tag.identifier) + break + case let .iso7816(tag): + metadata["kind"] = ["ISO7816Compatible"] + metadata["id"] = byteArrayFromData(tag.identifier) + break + default: + metadata["kind"] = ["Unknown"] + metadata["id"] = [] + break + } + + return metadata + } + + private func closeSession(_ session: NFCReaderSession) { + session.invalidate() + self.session = nil + } + + private func closeSession(_ session: NFCReaderSession, error: String) { + session.invalidate(errorMessage: error) + self.session = nil + } + + private func processTag( + session: NFCReaderSession, tag: T, metadata: JsonObject, mode: TagProcessMode + ) { + tag.queryNDEFStatus(completionHandler: { + [self] (status, capacity, error) in + if let error = error { + self.closeSession(session, error: "cannot connect to tag: \(error)") + } else { + switch mode { + case .write(let message): + self.writeNDEFTag( + session: session, status: status, tag: tag, message: message, + alertMessage: self.session?.successfulWriteAlertMessage) + break + case .read: + if self.session?.keepAlive == true { + self.session!.tagStatus = status + self.session!.tag = tag + } + self.readNDEFTag( + session: session, status: status, tag: tag, metadata: metadata, + alertMessage: self.session?.successfulReadMessage) + break + } + } + }) + } + + private func writeNDEFTag( + session: NFCReaderSession, status: NFCNDEFStatus, tag: T, message: NFCNDEFMessage, + alertMessage: String? + ) { + switch status { + case .notSupported: + self.closeSession(session, error: "Tag is not an NDEF-formatted tag") + break + case .readOnly: + self.closeSession(session, error: "Read only tag") + break + case .readWrite: + if let currentSession = self.session { + tag.writeNDEF( + message, + completionHandler: { (error) in + if let error = error { + self.closeSession(session, error: "cannot write to tag: \(error)") + } else { + if let message = alertMessage { + session.alertMessage = message + } + currentSession.invoke.resolve() + + self.closeSession(session) + + } + }) + } + break + default: + return + } + } + + private func readNDEFTag( + session: NFCReaderSession, status: NFCNDEFStatus, tag: T, metadata m: JsonObject, + alertMessage: String? + ) { + var metadata: JsonObject = [:] + metadata.merge(m) { (_, new) in new } + + switch status { + case .notSupported: + self.resolveInvoke(message: nil, metadata: metadata) + self.closeSession(session) + return + case .readOnly: + metadata["readOnly"] = true + break + case .readWrite: + metadata["readOnly"] = false + break + default: + break + } + + tag.readNDEF(completionHandler: { + [self] (message, error) in + if let error = error { + let code = (error as NSError).code + if code != 403 { + self.closeSession(session, error: "Failed to read: \(error)") + return + } + } + + if let message = alertMessage { + session.alertMessage = message + } + self.resolveInvoke(message: message, metadata: metadata) + + if self.session?.keepAlive != true { + self.closeSession(session) + } + }) + } + + private func resolveInvoke(message: NFCNDEFMessage?, metadata: JsonObject) { + var data: JsonObject = [:] + + data.merge(metadata) { (_, new) in new } + + if let message = message { + data["records"] = ndefMessageRecords(message) + } else { + data["records"] = [] + } + + self.session?.invoke.resolve(data) + } + + private func ndefMessageRecords(_ message: NFCNDEFMessage) -> [JsonObject] { + var records: [JsonObject] = [] + for record in message.records { + var recordJson: JsonObject = [:] + recordJson["tnf"] = record.typeNameFormat.rawValue + recordJson["kind"] = byteArrayFromData(record.type) + recordJson["id"] = byteArrayFromData(record.identifier) + recordJson["payload"] = byteArrayFromData(record.payload) + + records.append(recordJson) + } + + return records + } + + private func byteArrayFromData(_ data: Data) -> [UInt8] { + var arr: [UInt8] = [] + for b in data { + arr.append(b) + } + return arr + } + + private func dataFromByteArray(_ array: [UInt8]) -> Data { + var data = Data(capacity: array.count) + + data.append(contentsOf: array) + + return data + } + + @objc func isAvailable(_ invoke: Invoke) { + invoke.resolve([ + "available": self.status.available + ]) + } + + @objc public func write(_ invoke: Invoke) throws { + if !self.status.available { + invoke.reject("NFC reading unavailable: \(self.status.errorReason ?? "")") + return + } + + let args = try invoke.parseArgs(WriteOptions.self) + + var ndefPayloads = [NFCNDEFPayload]() + + for record in args.records { + ndefPayloads.append( + NFCNDEFPayload( + format: NFCTypeNameFormat(rawValue: record.format ?? 0) ?? .unknown, + type: dataFromByteArray(record.kind ?? []), + identifier: dataFromByteArray(record.identifier ?? []), + payload: dataFromByteArray(record.payload ?? []) + ) + ) + } + + if let session = self.session { + if let nfcSession = session.nfcSession, let tagStatus = session.tagStatus, + let tag = session.tag + { + session.keepAlive = false + self.writeNDEFTag( + session: nfcSession, status: tagStatus, tag: tag, + message: NFCNDEFMessage(records: ndefPayloads), + alertMessage: args.successMessage + ) + } else { + invoke.reject( + "connected tag not found, please wait for it to be available and then call write()") + } + } else { + self.startScanSession( + invoke: invoke, + kind: args.kind ?? .ndef, + keepAlive: true, + invalidateAfterFirstRead: false, + tagProcessMode: .write( + message: NFCNDEFMessage(records: ndefPayloads) + ), + alertMessage: args.message, + successfulReadMessage: args.successfulReadMessage, + successfulWriteAlertMessage: args.successMessage + ) + } + } + + @objc public func scan(_ invoke: Invoke) throws { + if !self.status.available { + invoke.reject("NFC reading unavailable: \(self.status.errorReason ?? "")") + return + } + + let args = try invoke.parseArgs(ScanOptions.self) + + self.startScanSession( + invoke: invoke, + kind: args.kind, + keepAlive: args.keepSessionAlive ?? false, + invalidateAfterFirstRead: true, + tagProcessMode: .read, + alertMessage: args.message, + successfulReadMessage: args.successMessage, + successfulWriteAlertMessage: nil + ) + } + + private func startScanSession( + invoke: Invoke, + kind: ScanKind, + keepAlive: Bool, + invalidateAfterFirstRead: Bool, + tagProcessMode: TagProcessMode, + alertMessage: String?, + successfulReadMessage: String?, + successfulWriteAlertMessage: String? + ) { + let nfcSession: NFCReaderSession? + + switch kind { + case .tag: + nfcSession = NFCTagReaderSession( + pollingOption: [.iso14443, .iso15693], + delegate: self, + queue: DispatchQueue.main + ) + break + case .ndef: + nfcSession = NFCNDEFReaderSession( + delegate: self, + queue: DispatchQueue.main, + invalidateAfterFirstRead: invalidateAfterFirstRead + ) + break + } + + if let message = alertMessage { + nfcSession?.alertMessage = message + } + nfcSession?.begin() + + self.session = Session( + nfcSession: nfcSession, + invoke: invoke, + keepAlive: keepAlive, + tagProcessMode: tagProcessMode, + successfulReadMessage: successfulReadMessage, + successfulWriteAlertMessage: successfulWriteAlertMessage + ) + } +} + +@_cdecl("init_plugin_nfc") +func initPlugin() -> Plugin { + return NfcPlugin() +} diff --git a/plugins/nfc/ios/Tests/PluginTests/PluginTests.swift b/plugins/nfc/ios/Tests/PluginTests/PluginTests.swift new file mode 100644 index 0000000..99992ce --- /dev/null +++ b/plugins/nfc/ios/Tests/PluginTests/PluginTests.swift @@ -0,0 +1,12 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +import XCTest +@testable import ExamplePlugin + +final class ExamplePluginTests: XCTestCase { + func testExample() throws { + let plugin = ExamplePlugin() + } +} diff --git a/plugins/nfc/package.json b/plugins/nfc/package.json new file mode 100644 index 0000000..366bf1c --- /dev/null +++ b/plugins/nfc/package.json @@ -0,0 +1,30 @@ +{ + "name": "@tauri-apps/plugin-nfc", + "version": "2.3.6", + "license": "MIT OR Apache-2.0", + "authors": [ + "Tauri Programme within The Commons Conservancy" + ], + "repository": "https://github.com/tauri-apps/plugins-workspace", + "type": "module", + "types": "./dist-js/index.d.ts", + "main": "./dist-js/index.cjs", + "module": "./dist-js/index.js", + "exports": { + "types": "./dist-js/index.d.ts", + "import": "./dist-js/index.js", + "require": "./dist-js/index.cjs" + }, + "scripts": { + "build": "rollup -c" + }, + "files": [ + "dist-js", + "!dist-js/**/*.map", + "README.md", + "LICENSE" + ], + "dependencies": { + "@tauri-apps/api": "^2.11.0" + } +} diff --git a/plugins/nfc/permissions/autogenerated/commands/is_available.toml b/plugins/nfc/permissions/autogenerated/commands/is_available.toml new file mode 100644 index 0000000..6f49c5a --- /dev/null +++ b/plugins/nfc/permissions/autogenerated/commands/is_available.toml @@ -0,0 +1,13 @@ +# Automatically generated - DO NOT EDIT! + +"$schema" = "../../schemas/schema.json" + +[[permission]] +identifier = "allow-is-available" +description = "Enables the is_available command without any pre-configured scope." +commands.allow = ["is_available"] + +[[permission]] +identifier = "deny-is-available" +description = "Denies the is_available command without any pre-configured scope." +commands.deny = ["is_available"] diff --git a/plugins/nfc/permissions/autogenerated/commands/scan.toml b/plugins/nfc/permissions/autogenerated/commands/scan.toml new file mode 100644 index 0000000..efa621d --- /dev/null +++ b/plugins/nfc/permissions/autogenerated/commands/scan.toml @@ -0,0 +1,13 @@ +# Automatically generated - DO NOT EDIT! + +"$schema" = "../../schemas/schema.json" + +[[permission]] +identifier = "allow-scan" +description = "Enables the scan command without any pre-configured scope." +commands.allow = ["scan"] + +[[permission]] +identifier = "deny-scan" +description = "Denies the scan command without any pre-configured scope." +commands.deny = ["scan"] diff --git a/plugins/nfc/permissions/autogenerated/commands/write.toml b/plugins/nfc/permissions/autogenerated/commands/write.toml new file mode 100644 index 0000000..73d1d38 --- /dev/null +++ b/plugins/nfc/permissions/autogenerated/commands/write.toml @@ -0,0 +1,13 @@ +# Automatically generated - DO NOT EDIT! + +"$schema" = "../../schemas/schema.json" + +[[permission]] +identifier = "allow-write" +description = "Enables the write command without any pre-configured scope." +commands.allow = ["write"] + +[[permission]] +identifier = "deny-write" +description = "Denies the write command without any pre-configured scope." +commands.deny = ["write"] diff --git a/plugins/nfc/permissions/autogenerated/reference.md b/plugins/nfc/permissions/autogenerated/reference.md new file mode 100644 index 0000000..0e408eb --- /dev/null +++ b/plugins/nfc/permissions/autogenerated/reference.md @@ -0,0 +1,103 @@ +## Default Permission + +This permission set configures what kind of +operations are available from the nfc plugin. + +#### Granted Permissions + +Checking if the NFC functionality is available +and scanning nearby tags is allowed. +Writing to tags needs to be manually enabled. + +#### This default permission set includes the following: + +- `allow-is-available` +- `allow-scan` + +## Permission Table + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
IdentifierDescription
+ +`nfc:allow-is-available` + + + +Enables the is_available command without any pre-configured scope. + +
+ +`nfc:deny-is-available` + + + +Denies the is_available command without any pre-configured scope. + +
+ +`nfc:allow-scan` + + + +Enables the scan command without any pre-configured scope. + +
+ +`nfc:deny-scan` + + + +Denies the scan command without any pre-configured scope. + +
+ +`nfc:allow-write` + + + +Enables the write command without any pre-configured scope. + +
+ +`nfc:deny-write` + + + +Denies the write command without any pre-configured scope. + +
diff --git a/plugins/nfc/permissions/default.toml b/plugins/nfc/permissions/default.toml new file mode 100644 index 0000000..d69c7f1 --- /dev/null +++ b/plugins/nfc/permissions/default.toml @@ -0,0 +1,15 @@ +"$schema" = "schemas/schema.json" + +[default] +description = """ +This permission set configures what kind of +operations are available from the nfc plugin. + +#### Granted Permissions + +Checking if the NFC functionality is available +and scanning nearby tags is allowed. +Writing to tags needs to be manually enabled. + +""" +permissions = ["allow-is-available", "allow-scan"] diff --git a/plugins/nfc/permissions/schemas/schema.json b/plugins/nfc/permissions/schemas/schema.json new file mode 100644 index 0000000..8a018e2 --- /dev/null +++ b/plugins/nfc/permissions/schemas/schema.json @@ -0,0 +1,342 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "PermissionFile", + "description": "Permission file that can define a default permission, a set of permissions or a list of inlined permissions.", + "type": "object", + "properties": { + "default": { + "description": "The default permission set for the plugin", + "anyOf": [ + { + "$ref": "#/definitions/DefaultPermission" + }, + { + "type": "null" + } + ] + }, + "set": { + "description": "A list of permissions sets defined", + "type": "array", + "items": { + "$ref": "#/definitions/PermissionSet" + } + }, + "permission": { + "description": "A list of inlined permissions", + "default": [], + "type": "array", + "items": { + "$ref": "#/definitions/Permission" + } + } + }, + "definitions": { + "DefaultPermission": { + "description": "The default permission set of the plugin.\n\nWorks similarly to a permission with the \"default\" identifier.", + "type": "object", + "required": [ + "permissions" + ], + "properties": { + "version": { + "description": "The version of the permission.", + "type": [ + "integer", + "null" + ], + "format": "uint64", + "minimum": 1.0 + }, + "description": { + "description": "Human-readable description of what the permission does. Tauri convention is to use `

` headings in markdown content for Tauri documentation generation purposes.", + "type": [ + "string", + "null" + ] + }, + "permissions": { + "description": "All permissions this set contains.", + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "PermissionSet": { + "description": "A set of direct permissions grouped together under a new name.", + "type": "object", + "required": [ + "description", + "identifier", + "permissions" + ], + "properties": { + "identifier": { + "description": "A unique identifier for the permission.", + "type": "string" + }, + "description": { + "description": "Human-readable description of what the permission does.", + "type": "string" + }, + "permissions": { + "description": "All permissions this set contains.", + "type": "array", + "items": { + "$ref": "#/definitions/PermissionKind" + } + } + } + }, + "Permission": { + "description": "Descriptions of explicit privileges of commands.\n\nIt can enable commands to be accessible in the frontend of the application.\n\nIf the scope is defined it can be used to fine grain control the access of individual or multiple commands.", + "type": "object", + "required": [ + "identifier" + ], + "properties": { + "version": { + "description": "The version of the permission.", + "type": [ + "integer", + "null" + ], + "format": "uint64", + "minimum": 1.0 + }, + "identifier": { + "description": "A unique identifier for the permission.", + "type": "string" + }, + "description": { + "description": "Human-readable description of what the permission does. Tauri internal convention is to use `

` headings in markdown content for Tauri documentation generation purposes.", + "type": [ + "string", + "null" + ] + }, + "commands": { + "description": "Allowed or denied commands when using this permission.", + "default": { + "allow": [], + "deny": [] + }, + "allOf": [ + { + "$ref": "#/definitions/Commands" + } + ] + }, + "scope": { + "description": "Allowed or denied scoped when using this permission.", + "allOf": [ + { + "$ref": "#/definitions/Scopes" + } + ] + }, + "platforms": { + "description": "Target platforms this permission applies. By default all platforms are affected by this permission.", + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/Target" + } + } + } + }, + "Commands": { + "description": "Allowed and denied commands inside a permission.\n\nIf two commands clash inside of `allow` and `deny`, it should be denied by default.", + "type": "object", + "properties": { + "allow": { + "description": "Allowed command.", + "default": [], + "type": "array", + "items": { + "type": "string" + } + }, + "deny": { + "description": "Denied command, which takes priority.", + "default": [], + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "Scopes": { + "description": "An argument for fine grained behavior control of Tauri commands.\n\nIt can be of any serde serializable type and is used to allow or prevent certain actions inside a Tauri command. The configured scope is passed to the command and will be enforced by the command implementation.\n\n## Example\n\n```json { \"allow\": [{ \"path\": \"$HOME/**\" }], \"deny\": [{ \"path\": \"$HOME/secret.txt\" }] } ```", + "type": "object", + "properties": { + "allow": { + "description": "Data that defines what is allowed by the scope.", + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/Value" + } + }, + "deny": { + "description": "Data that defines what is denied by the scope. This should be prioritized by validation logic.", + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/Value" + } + } + } + }, + "Value": { + "description": "All supported ACL values.", + "anyOf": [ + { + "description": "Represents a null JSON value.", + "type": "null" + }, + { + "description": "Represents a [`bool`].", + "type": "boolean" + }, + { + "description": "Represents a valid ACL [`Number`].", + "allOf": [ + { + "$ref": "#/definitions/Number" + } + ] + }, + { + "description": "Represents a [`String`].", + "type": "string" + }, + { + "description": "Represents a list of other [`Value`]s.", + "type": "array", + "items": { + "$ref": "#/definitions/Value" + } + }, + { + "description": "Represents a map of [`String`] keys to [`Value`]s.", + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/Value" + } + } + ] + }, + "Number": { + "description": "A valid ACL number.", + "anyOf": [ + { + "description": "Represents an [`i64`].", + "type": "integer", + "format": "int64" + }, + { + "description": "Represents a [`f64`].", + "type": "number", + "format": "double" + } + ] + }, + "Target": { + "description": "Platform target.", + "oneOf": [ + { + "description": "MacOS.", + "type": "string", + "enum": [ + "macOS" + ] + }, + { + "description": "Windows.", + "type": "string", + "enum": [ + "windows" + ] + }, + { + "description": "Linux.", + "type": "string", + "enum": [ + "linux" + ] + }, + { + "description": "Android.", + "type": "string", + "enum": [ + "android" + ] + }, + { + "description": "iOS.", + "type": "string", + "enum": [ + "iOS" + ] + } + ] + }, + "PermissionKind": { + "type": "string", + "oneOf": [ + { + "description": "Enables the is_available command without any pre-configured scope.", + "type": "string", + "const": "allow-is-available", + "markdownDescription": "Enables the is_available command without any pre-configured scope." + }, + { + "description": "Denies the is_available command without any pre-configured scope.", + "type": "string", + "const": "deny-is-available", + "markdownDescription": "Denies the is_available command without any pre-configured scope." + }, + { + "description": "Enables the scan command without any pre-configured scope.", + "type": "string", + "const": "allow-scan", + "markdownDescription": "Enables the scan command without any pre-configured scope." + }, + { + "description": "Denies the scan command without any pre-configured scope.", + "type": "string", + "const": "deny-scan", + "markdownDescription": "Denies the scan command without any pre-configured scope." + }, + { + "description": "Enables the write command without any pre-configured scope.", + "type": "string", + "const": "allow-write", + "markdownDescription": "Enables the write command without any pre-configured scope." + }, + { + "description": "Denies the write command without any pre-configured scope.", + "type": "string", + "const": "deny-write", + "markdownDescription": "Denies the write command without any pre-configured scope." + }, + { + "description": "This permission set configures what kind of\noperations are available from the nfc plugin.\n\n#### Granted Permissions\n\nChecking if the NFC functionality is available\nand scanning nearby tags is allowed.\nWriting to tags needs to be manually enabled.\n\n\n#### This default permission set includes:\n\n- `allow-is-available`\n- `allow-scan`", + "type": "string", + "const": "default", + "markdownDescription": "This permission set configures what kind of\noperations are available from the nfc plugin.\n\n#### Granted Permissions\n\nChecking if the NFC functionality is available\nand scanning nearby tags is allowed.\nWriting to tags needs to be manually enabled.\n\n\n#### This default permission set includes:\n\n- `allow-is-available`\n- `allow-scan`" + } + ] + } + } +} \ No newline at end of file diff --git a/plugins/nfc/rollup.config.js b/plugins/nfc/rollup.config.js new file mode 100644 index 0000000..1f349ec --- /dev/null +++ b/plugins/nfc/rollup.config.js @@ -0,0 +1,7 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +import { createConfig } from '../../shared/rollup.config.js' + +export default createConfig() diff --git a/plugins/nfc/src/error.rs b/plugins/nfc/src/error.rs new file mode 100644 index 0000000..75d21ea --- /dev/null +++ b/plugins/nfc/src/error.rs @@ -0,0 +1,34 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use serde::{ser::Serializer, Serialize}; + +/// Alias for a [`std::result::Result`] with the error type [`Error`]. +pub type Result = std::result::Result; + +/// Errors returned by the NFC plugin. +/// +/// Serializes to the error message string, so it can be returned directly from a command. +#[derive(Debug, thiserror::Error)] +pub enum Error { + /// An I/O error happened. + #[error(transparent)] + Io(#[from] std::io::Error), + /// The call to the Android or iOS plugin implementation failed, + /// either because the arguments could not be serialized, the response could not be + /// deserialized or the native side rejected the call (e.g. NFC is unavailable + /// or the tag could not be read or written). + #[cfg(mobile)] + #[error(transparent)] + PluginInvoke(#[from] tauri::plugin::mobile::PluginInvokeError), +} + +impl Serialize for Error { + fn serialize(&self, serializer: S) -> std::result::Result + where + S: Serializer, + { + serializer.serialize_str(self.to_string().as_ref()) + } +} diff --git a/plugins/nfc/src/lib.rs b/plugins/nfc/src/lib.rs new file mode 100644 index 0000000..0396b77 --- /dev/null +++ b/plugins/nfc/src/lib.rs @@ -0,0 +1,122 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +//! Read and write NFC tags on Android and iOS. +//! +//! This plugin is mobile only: the whole crate is gated behind `#[cfg(mobile)]`, +//! so it expands to nothing when compiling for Linux, macOS or Windows. +//! +//! Register the plugin with `init` and use the `NfcExt` trait to reach the +//! `Nfc` instance from any `tauri::Manager` implementation (the app handle, a window, ...). + +#![cfg(mobile)] + +use serde::{Deserialize, Serialize}; +use tauri::{ + plugin::{Builder, PluginHandle, TauriPlugin}, + Manager, Runtime, +}; + +pub use models::*; + +mod error; +mod models; + +pub use error::{Error, Result}; + +#[cfg(target_os = "android")] +const PLUGIN_IDENTIFIER: &str = "app.tauri.nfc"; + +#[cfg(target_os = "ios")] +tauri::ios_plugin_binding!(init_plugin_nfc); + +/// Access to the nfc APIs. +pub struct Nfc(PluginHandle); + +#[derive(Deserialize)] +struct IsAvailableResponse { + available: bool, +} + +#[derive(Serialize)] +struct WriteRequest { + records: Vec, +} + +impl Nfc { + /// Checks whether NFC is supported by the device and currently usable by the app. + /// + /// On Android this is `false` when the device has no NFC adapter or when NFC is + /// disabled in the device settings. + /// On iOS this is `false` when the `NFCReaderUsageDescription` entry is missing from the + /// `Info.plist` file or when NFC tag reading is not available on the device. + pub fn is_available(&self) -> crate::Result { + self.0 + .run_mobile_plugin::("isAvailable", ()) + .map(|r| r.available) + .map_err(Into::into) + } + + /// Scans an NFC tag, blocking until a tag matching the given [`ScanRequest::kind`] filters + /// is read or the scan fails. + /// + /// Set [`ScanRequest::keep_session_alive`] to `true` to keep the connection to the tag open + /// after it has been scanned, so that a following [`Self::write`] call writes to that tag. + /// + /// # Errors + /// + /// Returns an error when NFC is not available (see [`Self::is_available`]) + /// or when the tag could not be read. + pub fn scan(&self, payload: ScanRequest) -> crate::Result { + self.0 + .run_mobile_plugin("scan", payload) + .map(|v| ScanResponse { tag: v }) + .map_err(Into::into) + } + + /// Writes the given NDEF records to an NFC tag, blocking until the write completes or fails. + /// + /// Because this API does not take a scan kind, on Android it can only write to the tag of an + /// ongoing session, so it must be preceded by a [`Self::scan`] call with + /// [`ScanRequest::keep_session_alive`] set to `true`. + /// On iOS an NDEF reader session is started when there is no ongoing session, and the + /// records are written to the first tag that is scanned. + /// + /// # Errors + /// + /// Returns an error when NFC is not available (see [`Self::is_available`]), when there is no + /// connected tag on Android, when the tag is read-only, when it cannot hold the message or + /// when it does not support the NDEF format. + pub fn write(&self, records: Vec) -> crate::Result<()> { + self.0 + .run_mobile_plugin("write", WriteRequest { records }) + .map_err(Into::into) + } +} + +/// Extensions to [`tauri::App`], [`tauri::AppHandle`], [`tauri::WebviewWindow`], [`tauri::Webview`] and [`tauri::Window`] to access the NFC APIs. +pub trait NfcExt { + /// Returns the [`Nfc`] instance managed by the plugin. + fn nfc(&self) -> &Nfc; +} + +impl> crate::NfcExt for T { + fn nfc(&self) -> &Nfc { + self.state::>().inner() + } +} + +/// Initializes the plugin. +pub fn init() -> TauriPlugin { + Builder::new("nfc") + .setup(|app, api| { + #[cfg(target_os = "android")] + let handle = api.register_android_plugin(PLUGIN_IDENTIFIER, "NfcPlugin")?; + #[cfg(target_os = "ios")] + let handle = api.register_ios_plugin(init_plugin_nfc)?; + app.manage(Nfc(handle)); + Ok(()) + }) + .build() +} diff --git a/plugins/nfc/src/mobile.rs b/plugins/nfc/src/mobile.rs new file mode 100644 index 0000000..cf34d45 --- /dev/null +++ b/plugins/nfc/src/mobile.rs @@ -0,0 +1,11 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use serde::de::DeserializeOwned; +use tauri::{ + plugin::{PluginApi, PluginHandle}, + AppHandle, Runtime, +}; + +use crate::models::*; diff --git a/plugins/nfc/src/models.rs b/plugins/nfc/src/models.rs new file mode 100644 index 0000000..ccb4c55 --- /dev/null +++ b/plugins/nfc/src/models.rs @@ -0,0 +1,195 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use serde::{Deserialize, Serialize, Serializer}; +use std::fmt::Display; + +/// Arguments of the [`Nfc::scan`](crate::Nfc::scan) API. +#[derive(Serialize)] +#[serde(rename_all = "camelCase")] +pub struct ScanRequest { + /// The kind of scan to perform, which defines how tags are matched. + pub kind: ScanKind, + /// Whether the connection to the scanned tag must be kept open after the scan resolves. + /// + /// When `true`, a following [`Nfc::write`](crate::Nfc::write) call writes to the tag that was + /// scanned instead of starting a new session. + pub keep_session_alive: bool, +} + +/// An NDEF record to be written to a tag. +/// +/// Use [`NFCTypeNameFormat`] to describe how [`Self::kind`] must be interpreted. +#[derive(Serialize)] +#[serde(rename_all = "camelCase")] +pub struct NfcRecord { + /// The Type Name Format (TNF) of the record. + pub format: NFCTypeNameFormat, + /// The record type, interpreted according to [`Self::format`]. + /// + /// For [`NFCTypeNameFormat::NfcWellKnown`] records this is a Record Type Definition (RTD) + /// value such as `[0x54]` (`RTD_TEXT`) or `[0x55]` (`RTD_URI`). + pub kind: Vec, + /// The record identifier. Can be empty. + pub id: Vec, + /// The record payload bytes. + pub payload: Vec, +} + +/// The Type Name Format (TNF) of an NDEF record, which defines how the record type is interpreted. +/// +/// Serialized as its numeric value. +#[derive(serde_repr::Deserialize_repr, serde_repr::Serialize_repr)] +#[repr(u8)] +pub enum NFCTypeNameFormat { + /// The record is empty: type, identifier and payload must be empty. + Empty = 0, + /// The record type is an NFC Forum well known type, defined by a Record Type Definition (RTD) + /// such as `RTD_TEXT` (`[0x54]`) or `RTD_URI` (`[0x55]`). + NfcWellKnown = 1, + /// The record type is a MIME media type as defined in RFC 2046, e.g. `text/plain`. + Media = 2, + /// The record type is an absolute URI as defined in RFC 3986. + AbsoluteURI = 3, + /// The record type is an NFC Forum external type, i.e. a type namespaced by its issuer. + NfcExternal = 4, + /// The record type is unknown: the type must be empty and the payload interpretation is + /// left to the application. + Unknown = 5, + /// The record is a middle or last chunk of a chunked record and inherits the type of the + /// first chunk, so its own type must be empty. + Unchanged = 6, +} + +/// An NDEF record read from a scanned tag. +#[derive(Deserialize)] +pub struct NfcTagRecord { + /// The Type Name Format (TNF) of the record, which defines how [`Self::kind`] is interpreted. + pub tnf: NFCTypeNameFormat, + /// The record type bytes. + pub kind: Vec, + /// The record identifier bytes. Can be empty. + pub id: Vec, + /// The record payload bytes. + pub payload: Vec, +} + +/// An NFC tag that has been scanned. +#[derive(Deserialize)] +pub struct NfcTag { + /// The tag identifier, as reported by the operating system. + pub id: String, + /// The technology the tag supports. + pub kind: String, + /// The NDEF records stored on the tag. Empty when the tag holds no NDEF message. + pub records: Vec, +} + +/// Response of the [`Nfc::scan`](crate::Nfc::scan) API. +#[derive(Deserialize)] +pub struct ScanResponse { + /// The tag that has been scanned. + pub tag: NfcTag, +} + +/// Filters the tags to scan by the URI of their NDEF payload. +/// +/// Every field is optional and only the ones that are set take part in the filter. +/// **Android only**: the iOS implementation ignores this filter. +#[derive(Debug, Default, Serialize)] +pub struct UriFilter { + /// Only match URIs with this scheme, e.g. `https`. + scheme: Option, + /// Only match URIs with this authority (host), e.g. `tauri.app`. + host: Option, + /// Only match URIs whose path starts with this prefix, e.g. `/docs`. + path_prefix: Option, +} + +/// The NFC technologies a tag can support, mirroring the `android.nfc.tech` classes. +/// +/// **Android only**. Serialized as the technology name, e.g. `"IsoDep"`. +#[derive(Debug)] +pub enum TechKind { + /// ISO-DEP (ISO 14443-4) properties and I/O operations. + IsoDep, + /// MIFARE Classic properties and I/O operations. + MifareClassic, + /// MIFARE Ultralight and MIFARE Ultralight C properties and I/O operations. + MifareUltralight, + /// NDEF data and operations on tags that are already formatted as NDEF. + Ndef, + /// Formatting operations on tags that can be formatted as NDEF but are not yet. + NdefFormatable, + /// NFC-A (ISO 14443-3A) properties and I/O operations. + NfcA, + /// NFC-B (ISO 14443-3B) properties and I/O operations. + NfcB, + /// NFC Barcode (Kovio NFC Barcode) properties and I/O operations. + NfcBarcode, + /// NFC-F (JIS 6319-4) properties and I/O operations. + NfcF, + /// NFC-V (ISO 15693) properties and I/O operations. + NfcV, +} + +impl Display for TechKind { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "{}", + match self { + Self::IsoDep => "IsoDep", + Self::MifareClassic => "MifareClassic", + Self::MifareUltralight => "MifareUltralight", + Self::Ndef => "Ndef", + Self::NdefFormatable => "NdefFormatable", + Self::NfcA => "NfcA", + Self::NfcB => "NfcB", + Self::NfcBarcode => "NfcBarcode", + Self::NfcF => "NfcF", + Self::NfcV => "NfcV", + } + ) + } +} + +impl Serialize for TechKind { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + serializer.serialize_str(&self.to_string()) + } +} + +/// The kind of scan to perform, which defines which tags are matched. +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub enum ScanKind { + /// Only match tags that carry an NDEF message. + Ndef { + /// Only match tags whose NDEF payload has this MIME type, e.g. `text/plain`. + /// **Android only**. + mime_type: Option, + /// Only match tags whose NDEF payload URI matches this filter. **Android only**. + uri: Option, + /// Only match tags supporting the listed technologies. + /// + /// Each tech list is considered independently and the tag matches when any single tech + /// list matches it, which provides AND (inside a list) and OR (between lists) semantics. + /// + /// **Android only**. See + /// + /// for more information. + tech_list: Option>>, + }, + /// Match any tag that is discovered, whether it carries an NDEF message or not. + Tag { + /// Only match tags whose payload has this MIME type, e.g. `text/plain`. **Android only**. + mime_type: Option, + /// Only match tags whose payload URI matches this filter. **Android only**. + uri: Option, + }, +} diff --git a/plugins/nfc/tsconfig.json b/plugins/nfc/tsconfig.json new file mode 100644 index 0000000..4374468 --- /dev/null +++ b/plugins/nfc/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "rootDir": "guest-js" + }, + "include": ["guest-js/*.ts"] +} diff --git a/plugins/notification/CHANGELOG.md b/plugins/notification/CHANGELOG.md new file mode 100644 index 0000000..48c2f4a --- /dev/null +++ b/plugins/notification/CHANGELOG.md @@ -0,0 +1,165 @@ +# Changelog + +## [2.4.0] + +- [`db9c5998`](https://github.com/tauri-apps/plugins-workspace/commit/db9c5998feff9384f9cbbefcbe0d45937c00a1fc) ([#3531](https://github.com/tauri-apps/plugins-workspace/pull/3531) by [@Legend-Master](https://github.com/tauri-apps/plugins-workspace/../../Legend-Master)) Fix missing `consumer-rules.pro` on Gradle v9. +- [`304292d7`](https://github.com/tauri-apps/plugins-workspace/commit/304292d740a666d3903ab58329860fc0fdde2818) ([#2805](https://github.com/tauri-apps/plugins-workspace/pull/2805) by [@Innominus](https://github.com/tauri-apps/plugins-workspace/../../Innominus)) - Add Rust builders for mobile notification action types. + - Fix Android action-group entries being stored under the action type ID instead of sequential indices, causing later actions to overwrite earlier ones. + +## \[2.3.3] + +- [`93426f85`](https://github.com/tauri-apps/plugins-workspace/commit/93426f85120f49beb9f40222bff45185a32d54a9) Fixed an issue that caused docs.rs builds to fail. No user facing changes. + +## \[2.3.2] + +- [`6c9b61fb`](https://github.com/tauri-apps/plugins-workspace/commit/6c9b61fb658145d13893626112fc489f7458aa17) ([#3039](https://github.com/tauri-apps/plugins-workspace/pull/3039) by [@FabianLars](https://github.com/tauri-apps/plugins-workspace/../../FabianLars)) On Android, updated compileSdk to 36. + +## \[2.3.1] + +- [`8abb31ee`](https://github.com/tauri-apps/plugins-workspace/commit/8abb31ee59c68197102c0aa699d690b34646ec3c) ([#2905](https://github.com/tauri-apps/plugins-workspace/pull/2905) by [@ChristianPavilonis](https://github.com/tauri-apps/plugins-workspace/../../ChristianPavilonis)) Fix notification scheduling on iOS. +- [`2d03e2ea`](https://github.com/tauri-apps/plugins-workspace/commit/2d03e2eac2c19ad997d81d23836ab6a219252ffb) ([#2678](https://github.com/tauri-apps/plugins-workspace/pull/2678) by [@Keerthi421](https://github.com/tauri-apps/plugins-workspace/../../Keerthi421)) Added sound support for desktop notifications which was previously only available on mobile platforms. + +## \[2.3.0] + +- [`f209b2f2`](https://github.com/tauri-apps/plugins-workspace/commit/f209b2f23cb29133c97ad5961fb46ef794dbe063) ([#2804](https://github.com/tauri-apps/plugins-workspace/pull/2804) by [@renovate](https://github.com/tauri-apps/plugins-workspace/../../renovate)) Updated tauri to 2.6 + +## \[2.2.3] + +- [`f634e524`](https://github.com/tauri-apps/plugins-workspace/commit/f634e5248ebe428f8305a59f74c13fc15147fb8e) This is an "empty" release to update the plugins' source files on crates.io and docs.rs. This should fix docs.rs build failures for projects using tauri plugins as dependencies. + +## \[2.2.2] + +- [`a1b3fa27`](https://github.com/tauri-apps/plugins-workspace/commit/a1b3fa27f11022c9b6622b4fab12d93239eb05de) ([#2515](https://github.com/tauri-apps/plugins-workspace/pull/2515) by [@FabianLars](https://github.com/tauri-apps/plugins-workspace/../../FabianLars)) Re-exported the `Geolocation`, `Haptics`, `Notification`, and `Os` structs so that they show up on docs.rs. + +## \[2.2.1] + +- [`da5c59e2`](https://github.com/tauri-apps/plugins-workspace/commit/da5c59e2fe879d177e3cfd52fcacce85440423cb) ([#2271](https://github.com/tauri-apps/plugins-workspace/pull/2271) by [@renovate](https://github.com/tauri-apps/plugins-workspace/../../renovate)) Updated `zbus` dependency to version 5. No API changes. + +## \[2.2.0] + +- [`3a79266b`](https://github.com/tauri-apps/plugins-workspace/commit/3a79266b8cf96a55b1ae6339d725567d45a44b1d) ([#2173](https://github.com/tauri-apps/plugins-workspace/pull/2173) by [@FabianLars](https://github.com/tauri-apps/plugins-workspace/../../FabianLars)) Bumped all plugins to `v2.2.0`. From now, the versions for the Rust and JavaScript packages of each plugin will be in sync with each other. + +## \[2.0.1] + +- [`a1a82208`](https://github.com/tauri-apps/plugins-workspace/commit/a1a82208ed4ab87f83310be0dc95428aec9ab241) ([#1873](https://github.com/tauri-apps/plugins-workspace/pull/1873) by [@lucasfernog](https://github.com/tauri-apps/plugins-workspace/../../lucasfernog)) Downgrade MSRV to 1.77.2 to support Windows 7. + +## \[2.0.0] + +- [`e2c4dfb6`](https://github.com/tauri-apps/plugins-workspace/commit/e2c4dfb6af43e5dd8d9ceba232c315f5febd55c1) Update to tauri v2 stable release. + +## \[2.0.0-rc.5] + +- [`fb85e5dd`](https://github.com/tauri-apps/plugins-workspace/commit/fb85e5dd76688f3ae836890160f9bde843b70167) ([#1785](https://github.com/tauri-apps/plugins-workspace/pull/1785)) Update to tauri 2.0.0-rc.12. + +## \[2.0.0-rc.4] + +- [`3d301c65`](https://github.com/tauri-apps/plugins-workspace/commit/3d301c654e6f5e7f343e0e0cbb57648002e98f04) ([#1737](https://github.com/tauri-apps/plugins-workspace/pull/1737) by [@lucasfernog](https://github.com/tauri-apps/plugins-workspace/../../lucasfernog)) The notification body is now optional on iOS to match the other platforms. + +## \[2.0.0-rc.1] + +- [`e2e97db5`](https://github.com/tauri-apps/plugins-workspace/commit/e2e97db51983267f5be84d4f6f0278d58834d1f5) ([#1701](https://github.com/tauri-apps/plugins-workspace/pull/1701) by [@lucasfernog](https://github.com/tauri-apps/plugins-workspace/../../lucasfernog)) Use `PermissionState` from the `tauri` crate, which now also includes a "prompt with rationale" variant for Android (returned when your app must explain to the user why it needs the permission). +- [`e2e97db5`](https://github.com/tauri-apps/plugins-workspace/commit/e2e97db51983267f5be84d4f6f0278d58834d1f5) ([#1701](https://github.com/tauri-apps/plugins-workspace/pull/1701) by [@lucasfernog](https://github.com/tauri-apps/plugins-workspace/../../lucasfernog)) **Breaking change**: The permission type when using the API is now `'granted' | 'denied' | 'prompt' | 'prompt-with-rationale'` instead of `'granted' | 'denied' | 'default'` for consistency with Rust types. When using the `window.Notification` API the type is unchanged to match the Web API type. +- [`e2e97db5`](https://github.com/tauri-apps/plugins-workspace/commit/e2e97db51983267f5be84d4f6f0278d58834d1f5) ([#1701](https://github.com/tauri-apps/plugins-workspace/pull/1701) by [@lucasfernog](https://github.com/tauri-apps/plugins-workspace/../../lucasfernog)) Update to tauri 2.0.0-rc.8 + +## \[2.0.0-rc.2] + +- [`b9147758`](https://github.com/tauri-apps/plugins-workspace/commit/b914775898c2bee7ceb20bd17ee595005cd17a64) ([#1679](https://github.com/tauri-apps/plugins-workspace/pull/1679) by [@lucasfernog](https://github.com/tauri-apps/plugins-workspace/../../lucasfernog)) Explicitly set a minimum macOS version for the Swift package. + +## \[2.0.0-rc.1] + +### changes + +- [`6b079cfd`](https://github.com/tauri-apps/plugins-workspace/commit/6b079cfdd107c94abc2c7300f6af00bac3ff4040) ([#1649](https://github.com/tauri-apps/plugins-workspace/pull/1649) by [@ahqsoftwares](https://github.com/tauri-apps/plugins-workspace/../../ahqsoftwares)) Remove targetSdk from build.kts files as it is deprecated and will be removed from DSL v9.0 + +## \[2.0.0-rc.0] + +- [`9887d1`](https://github.com/tauri-apps/plugins-workspace/commit/9887d14bd0e971c4c0f5c1188fc4005d3fc2e29e) Update to tauri RC. + +## \[2.0.0-beta.8] + +- [`99d6ac0f`](https://github.com/tauri-apps/plugins-workspace/commit/99d6ac0f9506a6a4a1aa59c728157190a7441af6) ([#1606](https://github.com/tauri-apps/plugins-workspace/pull/1606) by [@FabianLars](https://github.com/tauri-apps/plugins-workspace/../../FabianLars)) The JS packages now specify the *minimum* `@tauri-apps/api` version instead of a single exact version. +- [`6de87966`](https://github.com/tauri-apps/plugins-workspace/commit/6de87966ecc00ad9d91c25be452f1f46bd2b7e1f) ([#1597](https://github.com/tauri-apps/plugins-workspace/pull/1597) by [@Legend-Master](https://github.com/tauri-apps/plugins-workspace/../../Legend-Master)) Update to tauri beta.25. + +## \[2.0.0-beta.11] + +- [`725ff429`](https://github.com/tauri-apps/plugins-workspace/commit/725ff4295e56df9c30c099813bd64b96fe61b945) ([#1556](https://github.com/tauri-apps/plugins-workspace/pull/1556) by [@FabianLars](https://github.com/tauri-apps/plugins-workspace/../../FabianLars)) Fixed an issue that caused the `notification` plugin's initialization script to cause the WebView on Windows to throw a `STATUS_ACCESS_VIOLATION` error on remote websites. + +## \[2.0.0-beta.7] + +- [`22a17980`](https://github.com/tauri-apps/plugins-workspace/commit/22a17980ff4f6f8c40adb1b8f4ffc6dae2fe7e30) ([#1537](https://github.com/tauri-apps/plugins-workspace/pull/1537) by [@lucasfernog](https://github.com/tauri-apps/plugins-workspace/../../lucasfernog)) Update to tauri beta.24. + +## \[2.0.0-beta.6] + +- [`76daee7a`](https://github.com/tauri-apps/plugins-workspace/commit/76daee7aafece34de3092c86e531cf9eb1138989) ([#1512](https://github.com/tauri-apps/plugins-workspace/pull/1512) by [@renovate](https://github.com/tauri-apps/plugins-workspace/../../renovate)) Update to tauri beta.23. + +## \[2.0.0-beta.8] + +- [`3779fb50`](https://github.com/tauri-apps/plugins-workspace/commit/3779fb50634fba4d7e7eb0bfecc2216349b9d64d) ([#1432](https://github.com/tauri-apps/plugins-workspace/pull/1432) by [@lucasfernog](https://github.com/tauri-apps/plugins-workspace/../../lucasfernog)) Use notify_rust from crates.io instead of local fork. + +## \[2.0.0-beta.5] + +- [`9013854f`](https://github.com/tauri-apps/plugins-workspace/commit/9013854f42a49a230b9dbb9d02774765528a923f)([#1382](https://github.com/tauri-apps/plugins-workspace/pull/1382)) Update to tauri beta.22. + +## \[2.0.0-beta.4] + +- [`430bd6f4`](https://github.com/tauri-apps/plugins-workspace/commit/430bd6f4f379bee5d232ae6b098ae131db7f178a)([#1363](https://github.com/tauri-apps/plugins-workspace/pull/1363)) Update to tauri beta.20. + +## \[2.0.0-beta.3] + +- [`bd1ed590`](https://github.com/tauri-apps/plugins-workspace/commit/bd1ed5903ffcce5500310dac1e59e8c67674ef1e)([#1237](https://github.com/tauri-apps/plugins-workspace/pull/1237)) Update to tauri beta.17. + +## \[2.0.0-beta.4] + +- [`326df688`](https://github.com/tauri-apps/plugins-workspace/commit/326df6883998d416fc0837583ed972854628bb52)([#1236](https://github.com/tauri-apps/plugins-workspace/pull/1236)) Fixes command argument parsing on iOS. + +## \[2.0.0-beta.3] + +- [`a04ea2f`](https://github.com/tauri-apps/plugins-workspace/commit/a04ea2f38294d5a3987578283badc8eec87a7752)([#1071](https://github.com/tauri-apps/plugins-workspace/pull/1071)) The global API script is now only added to the binary when the `withGlobalTauri` config is true. +- [`62ce5df`](https://github.com/tauri-apps/plugins-workspace/commit/62ce5df52ca3c86786e711ef193a206e7b0dc0cf)([#1096](https://github.com/tauri-apps/plugins-workspace/pull/1096)) Fix development mode check to set the app ID on macOS. + +## \[2.0.0-beta.2] + +- [`99bea25`](https://github.com/tauri-apps/plugins-workspace/commit/99bea2559c2c0648c2519c50a18cd124dacef57b)([#1005](https://github.com/tauri-apps/plugins-workspace/pull/1005)) Update to tauri beta.8. + +## \[2.0.0-beta.1] + +- [`569defb`](https://github.com/tauri-apps/plugins-workspace/commit/569defbe9492e38938554bb7bdc1be9151456d21) Update to tauri beta.4. + +## \[2.0.0-beta.0] + +- [`d198c01`](https://github.com/tauri-apps/plugins-workspace/commit/d198c014863ee260cb0de88a14b7fc4356ef7474)([#862](https://github.com/tauri-apps/plugins-workspace/pull/862)) Update to tauri beta. +- [`1b1d795`](https://github.com/tauri-apps/plugins-workspace/commit/1b1d795b5866e5524a9a9925f0fb7b2f8e3e3675)([#874](https://github.com/tauri-apps/plugins-workspace/pull/874)) Export the missing `Schedule` class. +- [`8dea78a`](https://github.com/tauri-apps/plugins-workspace/commit/8dea78ac7dcb502159e66bad464094696aa257d4)([#909](https://github.com/tauri-apps/plugins-workspace/pull/909)) Fixes deserialization and implementation bugs with scheduled notifications on Android. + +## \[2.0.0-alpha.5] + +- [`387c2f9`](https://github.com/tauri-apps/plugins-workspace/commit/387c2f9e0ce4c75c07ffa3fd76391a25b58f5daf)([#802](https://github.com/tauri-apps/plugins-workspace/pull/802)) Update to @tauri-apps/api v2.0.0-alpha.13. + +## \[2.0.0-alpha.4] + +- [`387c2f9`](https://github.com/tauri-apps/plugins-workspace/commit/387c2f9e0ce4c75c07ffa3fd76391a25b58f5daf)([#802](https://github.com/tauri-apps/plugins-workspace/pull/802)) Update to @tauri-apps/api v2.0.0-alpha.12. + +## \[2.0.0-alpha.3] + +- [`e438e0a`](https://github.com/tauri-apps/plugins-workspace/commit/e438e0a62d4b430a5159f05f13ecd397dd891a0d)([#676](https://github.com/tauri-apps/plugins-workspace/pull/676)) Update to @tauri-apps/api v2.0.0-alpha.11. + +## \[2.0.0-alpha.2] + +- [`5c13736`](https://github.com/tauri-apps/plugins-workspace/commit/5c137365c60790e8d4037d449e8237aa3fffdab0)([#673](https://github.com/tauri-apps/plugins-workspace/pull/673)) Update to @tauri-apps/api v2.0.0-alpha.9. + +## \[2.0.0-alpha.3] + +- [`4e2cef9`](https://github.com/tauri-apps/plugins-workspace/commit/4e2cef9b702bbbb9cf4ee17de50791cb21f1b2a4)([#593](https://github.com/tauri-apps/plugins-workspace/pull/593)) Update to alpha.12. + +## \[2.0.0-alpha.1] + +- [`d74fc0a`](https://github.com/tauri-apps/plugins-workspace/commit/d74fc0a097996e90a37be8f57d50b7d1f6ca616f)([#555](https://github.com/tauri-apps/plugins-workspace/pull/555)) Update to alpha.11. + +## \[2.0.0-alpha.1] + +- [`d8b4aca`](https://github.com/tauri-apps/plugins-workspace/commit/d8b4aca69f628b170804ecb982e2c319d026ef47)([#414](https://github.com/tauri-apps/plugins-workspace/pull/414)) Use `window.__TAURI_INVOKE__` instead of `window.__TAURI__` in init.js, fixes usage in apps without `withGlobalTauri` enabled. +- [`7d71ad4`](https://github.com/tauri-apps/plugins-workspace/commit/7d71ad4e587bcf47ea34645f5b226945e487b765) Play a default sound when showing a notification on Windows. + +## \[2.0.0-alpha.0] + +- [`717ae67`](https://github.com/tauri-apps/plugins-workspace/commit/717ae670978feb4492fac1f295998b93f2b9347f)([#371](https://github.com/tauri-apps/plugins-workspace/pull/371)) First v2 alpha release! diff --git a/plugins/notification/Cargo.toml b/plugins/notification/Cargo.toml new file mode 100644 index 0000000..b306b5f --- /dev/null +++ b/plugins/notification/Cargo.toml @@ -0,0 +1,58 @@ +[package] +name = "tauri-plugin-notification" +version = "2.4.0" +description = "Send desktop and mobile notifications on your Tauri application." +edition = { workspace = true } +authors = { workspace = true } +license = { workspace = true } +rust-version = { workspace = true } +repository = { workspace = true } +links = "tauri-plugin-notification" +exclude = [ + "/banner.png", + "/guest-js", + "/package.json", + "/rollup.config.js", + "/test", + "/tsconfig.json", +] + +[package.metadata.docs.rs] +targets = ["x86_64-unknown-linux-gnu", "x86_64-linux-android"] + +[package.metadata.platforms.support] +windows = { level = "full", notes = "Only works for installed apps. Shows powershell name & icon in development." } +linux = { level = "full", notes = "" } +macos = { level = "full", notes = "" } +android = { level = "full", notes = "" } +ios = { level = "full", notes = "" } + +[build-dependencies] +tauri-plugin = { workspace = true, features = ["build"] } + +[dev-dependencies] +tauri = { workspace = true, features = ["wry"] } + +[dependencies] +serde = { workspace = true } +serde_json = { workspace = true } +tauri = { workspace = true } +log = { workspace = true } +thiserror = { workspace = true } +rand = "0.9" +time = { version = "0.3", features = ["serde", "parsing", "formatting"] } +url = { version = "2", features = ["serde"] } +serde_repr = "0.1" + +[target.'cfg(target_os = "ios")'.dependencies] +tauri = { workspace = true, features = ["wry"] } + +[target."cfg(windows)".dependencies] +win7-notifications = { version = "0.4.5", optional = true } +windows-version = { version = "0.1", optional = true } + +[target."cfg(any(target_os = \"macos\", windows, target_os = \"linux\", target_os = \"dragonfly\", target_os = \"freebsd\", target_os = \"openbsd\", target_os = \"netbsd\"))".dependencies] +notify-rust = "4.11" + +[features] +windows7-compat = ["win7-notifications", "windows-version"] diff --git a/plugins/notification/LICENSE.spdx b/plugins/notification/LICENSE.spdx new file mode 100644 index 0000000..cdd0df5 --- /dev/null +++ b/plugins/notification/LICENSE.spdx @@ -0,0 +1,20 @@ +SPDXVersion: SPDX-2.1 +DataLicense: CC0-1.0 +PackageName: tauri +DataFormat: SPDXRef-1 +PackageSupplier: Organization: The Tauri Programme in the Commons Conservancy +PackageHomePage: https://tauri.app +PackageLicenseDeclared: Apache-2.0 +PackageLicenseDeclared: MIT +PackageCopyrightText: 2019-2022, The Tauri Programme in the Commons Conservancy +PackageSummary: Tauri is a rust project that enables developers to make secure +and small desktop applications using a web frontend. + +PackageComment: The package includes the following libraries; see +Relationship information. + +Created: 2019-05-20T09:00:00Z +PackageDownloadLocation: git://github.com/tauri-apps/tauri +PackageDownloadLocation: git+https://github.com/tauri-apps/tauri.git +PackageDownloadLocation: git+ssh://github.com/tauri-apps/tauri.git +Creator: Person: Daniel Thompson-Yvetot \ No newline at end of file diff --git a/plugins/notification/LICENSE_APACHE-2.0 b/plugins/notification/LICENSE_APACHE-2.0 new file mode 100644 index 0000000..4947287 --- /dev/null +++ b/plugins/notification/LICENSE_APACHE-2.0 @@ -0,0 +1,177 @@ + + 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 \ No newline at end of file diff --git a/plugins/notification/LICENSE_MIT b/plugins/notification/LICENSE_MIT new file mode 100644 index 0000000..4d75472 --- /dev/null +++ b/plugins/notification/LICENSE_MIT @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2017 - Present Tauri Apps 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. \ No newline at end of file diff --git a/plugins/notification/README.md b/plugins/notification/README.md new file mode 100644 index 0000000..e17efe0 --- /dev/null +++ b/plugins/notification/README.md @@ -0,0 +1,161 @@ +![plugin-notification](https://github.com/tauri-apps/plugins-workspace/raw/v2/plugins/notification/banner.png) + +Send message notifications (brief auto-expiring OS window element) to your user. Can also be used with the Notification Web API. + +| Platform | Supported | +| -------- | --------- | +| Linux | ✓ | +| Windows | ✓ | +| macOS | ✓ | +| Android | ✓ | +| iOS | ✓ | + +## Install + +_This plugin requires a Rust version of at least **1.77.2**_ + +There are three general methods of installation that we can recommend. + +1. Use crates.io and npm (easiest, and requires you to trust that our publishing pipeline worked) +2. Pull sources directly from Github using git tags / revision hashes (most secure) +3. Git submodule install this repo in your tauri project and then use file protocol to ingest the source (most secure, but inconvenient to use) + +Install the Core plugin by adding the following to your `Cargo.toml` file: + +`src-tauri/Cargo.toml` + +```toml +[dependencies] +tauri-plugin-notification = "2.0.0" +# alternatively with Git: +tauri-plugin-notification = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v2" } +``` + +You can install the JavaScript Guest bindings using your preferred JavaScript package manager: + +```sh +pnpm add @tauri-apps/plugin-notification +# or +npm add @tauri-apps/plugin-notification +# or +yarn add @tauri-apps/plugin-notification +``` + +## Usage + +First you need to register the core plugin with Tauri: + +`src-tauri/src/lib.rs` + +```rust +fn main() { + tauri::Builder::default() + .plugin(tauri_plugin_notification::init()) + .run(tauri::generate_context!()) + .expect("error while running tauri application"); +} +``` + +Then you need to add the permissions to your capabilities file: + +`src-tauri/capabilities/main.json` + +```json +{ + ... + "permissions": [ + ... + "notification:default" + ], + ... +} +``` + +Afterwards all the plugin's APIs are available through the JavaScript guest bindings: + +```javascript +import { + isPermissionGranted, + requestPermission, + sendNotification +} from '@tauri-apps/plugin-notification' + +async function checkPermission() { + if (!(await isPermissionGranted())) { + return (await requestPermission()) === 'granted' + } + return true +} + +export async function enqueueNotification(title, body) { + if (!(await checkPermission())) { + return + } + sendNotification({ title, body }) +} +``` + +### Notification with Sound + +You can add sound to your notifications on all platforms (desktop and mobile): + +```javascript +import { sendNotification } from '@tauri-apps/plugin-notification' +import { platform } from '@tauri-apps/api/os' + +// Basic notification with sound +sendNotification({ + title: 'New Message', + body: 'You have a new message', + sound: 'notification.wav' // Path to sound file +}) + +// Platform-specific sounds +async function sendPlatformSpecificNotification() { + const platformName = platform() + + let soundPath + if (platformName === 'darwin') { + // On macOS: use system sounds or sound files in the app bundle + soundPath = 'Ping' // macOS system sound + } else if (platformName === 'linux') { + // On Linux: use XDG theme sounds or file paths + soundPath = 'message-new-instant' // XDG theme sound + } else { + // On Windows: use file paths + soundPath = 'notification.wav' + } + + sendNotification({ + title: 'Platform-specific Notification', + body: 'This notification uses platform-specific sound', + sound: soundPath + }) +} +``` + +## Contributing + +PRs accepted. Please make sure to read the Contributing Guide before making a pull request. + +## Partners + + + + + + + +
+ + CrabNebula + +
+ +For the complete list of sponsors please visit our [website](https://tauri.app#sponsors) and [Open Collective](https://opencollective.com/tauri). + +## License + +Code: (c) 2015 - Present - The Tauri Programme within The Commons Conservancy. + +MIT or MIT/Apache 2.0 where applicable. diff --git a/plugins/notification/SECURITY.md b/plugins/notification/SECURITY.md new file mode 100644 index 0000000..4f09bba --- /dev/null +++ b/plugins/notification/SECURITY.md @@ -0,0 +1,23 @@ +# Security Policy + +**Do not report security vulnerabilities through public GitHub issues.** + +**Please use the [Private Vulnerability Disclosure](https://docs.github.com/en/code-security/security-advisories/guidance-on-reporting-and-writing-information-about-vulnerabilities/privately-reporting-a-security-vulnerability#privately-reporting-a-security-vulnerability) feature of GitHub.** + +Include as much of the following information: + +- Type of issue (e.g. improper input parsing, privilege escalation, etc.) +- The location of the affected source code (tag/branch/commit or direct URL) +- Any special configuration required to reproduce the issue +- The distribution affected or used to help us with reproduction of the issue +- Step-by-step instructions to reproduce the issue +- Ideally a reproduction repository +- Impact of the issue, including how an attacker might exploit the issue + +We prefer to receive reports in English. + +## Contact + +Please disclose a vulnerability or security relevant issue here: [https://github.com/tauri-apps/plugins-workspace/security/advisories/new](https://github.com/tauri-apps/plugins-workspace/security/advisories/new). + +Alternatively, you can also contact us by email via [security@tauri.app](mailto:security@tauri.app). diff --git a/plugins/notification/android/.gitignore b/plugins/notification/android/.gitignore new file mode 100644 index 0000000..c0f21ec --- /dev/null +++ b/plugins/notification/android/.gitignore @@ -0,0 +1,2 @@ +/build +/.tauri diff --git a/plugins/notification/android/build.gradle.kts b/plugins/notification/android/build.gradle.kts new file mode 100644 index 0000000..f2b4cf6 --- /dev/null +++ b/plugins/notification/android/build.gradle.kts @@ -0,0 +1,36 @@ +plugins { + id("com.android.library") + id("org.jetbrains.kotlin.android") +} + +android { + namespace = "app.tauri.notification" + compileSdk = 36 + + defaultConfig { + minSdk = 24 + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + consumerProguardFiles("consumer-rules.pro") + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 + } + kotlinOptions { + jvmTarget = "1.8" + } +} + +dependencies { + + implementation("androidx.core:core-ktx:1.9.0") + implementation("androidx.appcompat:appcompat:1.6.0") + implementation("com.google.android.material:material:1.7.0") + implementation("com.fasterxml.jackson.core:jackson-databind:2.15.3") + testImplementation("junit:junit:4.13.2") + androidTestImplementation("androidx.test.ext:junit:1.1.5") + androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1") + implementation(project(":tauri-android")) +} diff --git a/plugins/notification/android/consumer-rules.pro b/plugins/notification/android/consumer-rules.pro new file mode 100644 index 0000000..481bb43 --- /dev/null +++ b/plugins/notification/android/consumer-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/plugins/notification/android/settings.gradle b/plugins/notification/android/settings.gradle new file mode 100644 index 0000000..14a752e --- /dev/null +++ b/plugins/notification/android/settings.gradle @@ -0,0 +1,2 @@ +include ':tauri-android' +project(':tauri-android').projectDir = new File('./.tauri/tauri-api') diff --git a/plugins/notification/android/src/androidTest/java/ExampleInstrumentedTest.kt b/plugins/notification/android/src/androidTest/java/ExampleInstrumentedTest.kt new file mode 100644 index 0000000..88ede7f --- /dev/null +++ b/plugins/notification/android/src/androidTest/java/ExampleInstrumentedTest.kt @@ -0,0 +1,28 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +package app.tauri.notification + +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.ext.junit.runners.AndroidJUnit4 + +import org.junit.Test +import org.junit.runner.RunWith + +import org.junit.Assert.* + +/** + * Instrumented test, which will execute on an Android device. + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +@RunWith(AndroidJUnit4::class) +class ExampleInstrumentedTest { + @Test + fun useAppContext() { + // Context of the app under test. + val appContext = InstrumentationRegistry.getInstrumentation().targetContext + assertEquals("app.tauri.notification", appContext.packageName) + } +} diff --git a/plugins/notification/android/src/main/AndroidManifest.xml b/plugins/notification/android/src/main/AndroidManifest.xml new file mode 100644 index 0000000..c49808e --- /dev/null +++ b/plugins/notification/android/src/main/AndroidManifest.xml @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/plugins/notification/android/src/main/java/AssetUtils.kt b/plugins/notification/android/src/main/java/AssetUtils.kt new file mode 100644 index 0000000..fafccf7 --- /dev/null +++ b/plugins/notification/android/src/main/java/AssetUtils.kt @@ -0,0 +1,29 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +package app.tauri.notification + +import android.annotation.SuppressLint +import android.content.Context + +class AssetUtils { + companion object { + const val RESOURCE_ID_ZERO_VALUE = 0 + + @SuppressLint("DiscouragedApi") + fun getResourceID(context: Context, resourceName: String?, dir: String?): Int { + return context.resources.getIdentifier(resourceName, dir, context.packageName) + } + + fun getResourceBaseName(resPath: String?): String? { + if (resPath == null) return null + if (resPath.contains("/")) { + return resPath.substring(resPath.lastIndexOf('/') + 1) + } + return if (resPath.contains(".")) { + resPath.substring(0, resPath.lastIndexOf('.')) + } else resPath + } + } +} diff --git a/plugins/notification/android/src/main/java/ChannelManager.kt b/plugins/notification/android/src/main/java/ChannelManager.kt new file mode 100644 index 0000000..206f340 --- /dev/null +++ b/plugins/notification/android/src/main/java/ChannelManager.kt @@ -0,0 +1,151 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +package app.tauri.notification + +import android.app.NotificationChannel +import android.app.NotificationManager +import android.content.ContentResolver +import android.content.Context +import android.graphics.Color +import android.media.AudioAttributes +import android.net.Uri +import android.os.Build +import app.tauri.Logger +import app.tauri.annotation.InvokeArg +import app.tauri.plugin.Invoke +import com.fasterxml.jackson.annotation.JsonValue + +enum class Importance(@JsonValue val value: Int) { + None(0), + Min(1), + Low(2), + Default(3), + High(4); +} + +enum class Visibility(@JsonValue val value: Int) { + Secret(-1), + Private(0), + Public(1); +} + +@InvokeArg +class Channel { + lateinit var id: String + lateinit var name: String + var description: String? = null + var sound: String? = null + var lights: Boolean? = null + var lightsColor: String? = null + var vibration: Boolean? = null + var importance: Importance? = null + var visibility: Visibility? = null +} + +@InvokeArg +class DeleteChannelArgs { + lateinit var id: String +} + +class ChannelManager(private var context: Context) { + private var notificationManager: NotificationManager? = null + + init { + notificationManager = + context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager? + } + + fun createChannel(invoke: Invoke) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + val channel = invoke.parseArgs(Channel::class.java) + createChannel(channel) + invoke.resolve() + } else { + invoke.reject("channel not available") + } + } + + private fun createChannel(channel: Channel) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + val notificationChannel = NotificationChannel( + channel.id, + channel.name, + (channel.importance ?: Importance.Default).value + ) + notificationChannel.description = channel.description + notificationChannel.lockscreenVisibility = (channel.visibility ?: Visibility.Private).value + notificationChannel.enableVibration(channel.vibration ?: false) + notificationChannel.enableLights(channel.lights ?: false) + val lightColor = channel.lightsColor ?: "" + if (lightColor.isNotEmpty()) { + try { + notificationChannel.lightColor = Color.parseColor(lightColor) + } catch (ex: IllegalArgumentException) { + Logger.error( + Logger.tags("NotificationChannel"), + "Invalid color provided for light color.", + null + ) + } + } + var sound = channel.sound ?: "" + if (sound.isNotEmpty()) { + if (sound.contains(".")) { + sound = sound.substring(0, sound.lastIndexOf('.')) + } + val audioAttributes = AudioAttributes.Builder() + .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION) + .setUsage(AudioAttributes.USAGE_NOTIFICATION) + .build() + val soundUri = + Uri.parse(ContentResolver.SCHEME_ANDROID_RESOURCE + "://" + context.packageName + "/raw/" + sound) + notificationChannel.setSound(soundUri, audioAttributes) + } + notificationManager?.createNotificationChannel(notificationChannel) + } + } + + fun deleteChannel(invoke: Invoke) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + val args = invoke.parseArgs(DeleteChannelArgs::class.java) + notificationManager?.deleteNotificationChannel(args.id) + invoke.resolve() + } else { + invoke.reject("channel not available") + } + } + + fun listChannels(invoke: Invoke) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + val notificationChannels: List = + notificationManager?.notificationChannels ?: listOf() + + val channels = mutableListOf() + + for (notificationChannel in notificationChannels) { + val channel = Channel() + channel.id = notificationChannel.id + channel.name = notificationChannel.name.toString() + channel.description = notificationChannel.description + channel.sound = notificationChannel.sound.toString() + channel.lights = notificationChannel.shouldShowLights() + String.format( + "#%06X", + 0xFFFFFF and notificationChannel.lightColor + ) + channel.vibration = notificationChannel.shouldVibrate() + channel.importance = Importance.values().firstOrNull { it.value == notificationChannel.importance } + channel.visibility = Visibility.values().firstOrNull { it.value == notificationChannel.lockscreenVisibility } + + channels.add(channel) + } + + invoke.resolveObject(channels) + + } else { + invoke.reject("channel not available") + } + } +} \ No newline at end of file diff --git a/plugins/notification/android/src/main/java/Notification.kt b/plugins/notification/android/src/main/java/Notification.kt new file mode 100644 index 0000000..bf10f3d --- /dev/null +++ b/plugins/notification/android/src/main/java/Notification.kt @@ -0,0 +1,95 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +package app.tauri.notification + +import android.content.ContentResolver +import android.content.Context +import android.graphics.Bitmap +import android.graphics.BitmapFactory +import app.tauri.annotation.InvokeArg +import app.tauri.plugin.JSArray +import app.tauri.plugin.JSObject +import org.json.JSONException +import org.json.JSONObject + +@InvokeArg +class Notification { + var id: Int = 0 + var title: String? = null + var body: String? = null + var largeBody: String? = null + var summary: String? = null + var sound: String? = null + var icon: String? = null + var largeIcon: String? = null + var iconColor: String? = null + var actionTypeId: String? = null + var group: String? = null + var inboxLines: List? = null + var isGroupSummary = false + var isOngoing = false + var isAutoCancel = false + var extra: JSObject? = null + var attachments: List? = null + var schedule: NotificationSchedule? = null + var channelId: String? = null + var sourceJson: String? = null + var visibility: Int? = null + var number: Int? = null + + fun getSound(context: Context, defaultSound: Int): String? { + var soundPath: String? = null + var resId: Int = AssetUtils.RESOURCE_ID_ZERO_VALUE + val name = AssetUtils.getResourceBaseName(sound) + if (name != null) { + resId = AssetUtils.getResourceID(context, name, "raw") + } + if (resId == AssetUtils.RESOURCE_ID_ZERO_VALUE) { + resId = defaultSound + } + if (resId != AssetUtils.RESOURCE_ID_ZERO_VALUE) { + soundPath = + ContentResolver.SCHEME_ANDROID_RESOURCE + "://" + context.packageName + "/" + resId + } + return soundPath + } + + fun getIconColor(globalColor: String): String { + // use the one defined local before trying for a globally defined color + return iconColor ?: globalColor + } + + fun getSmallIcon(context: Context, defaultIcon: Int): Int { + var resId: Int = AssetUtils.RESOURCE_ID_ZERO_VALUE + if (icon != null) { + resId = AssetUtils.getResourceID(context, icon, "drawable") + } + if (resId == AssetUtils.RESOURCE_ID_ZERO_VALUE) { + resId = defaultIcon + } + return resId + } + + fun getLargeIcon(context: Context): Bitmap? { + if (largeIcon != null) { + val resId: Int = AssetUtils.getResourceID(context, largeIcon, "drawable") + return BitmapFactory.decodeResource(context.resources, resId) + } + return null + } + + companion object { + fun buildNotificationPendingList(notifications: List): List { + val pendingNotifications = mutableListOf() + for (notification in notifications) { + val pendingNotification = PendingNotification(notification.id, notification.title, notification.body, notification.schedule, notification.extra) + pendingNotifications.add(pendingNotification) + } + return pendingNotifications + } + } +} + +class PendingNotification(val id: Int, val title: String?, val body: String?, val schedule: NotificationSchedule?, val extra: JSObject?) \ No newline at end of file diff --git a/plugins/notification/android/src/main/java/NotificationAttachment.kt b/plugins/notification/android/src/main/java/NotificationAttachment.kt new file mode 100644 index 0000000..56a1381 --- /dev/null +++ b/plugins/notification/android/src/main/java/NotificationAttachment.kt @@ -0,0 +1,52 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +package app.tauri.notification + +import app.tauri.plugin.JSObject +import org.json.JSONArray +import org.json.JSONException +import org.json.JSONObject + +class NotificationAttachment { + var id: String? = null + var url: String? = null + var options: JSONObject? = null + + companion object { + fun getAttachments(notification: JSObject): List { + val attachmentsList: MutableList = ArrayList() + var attachments: JSONArray? = null + try { + attachments = notification.getJSONArray("attachments") + } catch (_: Exception) { + } + if (attachments != null) { + for (i in 0 until attachments.length()) { + val newAttachment = NotificationAttachment() + var jsonObject: JSONObject? = null + try { + jsonObject = attachments.getJSONObject(i) + } catch (e: JSONException) { + } + if (jsonObject != null) { + var jsObject: JSObject? = null + try { + jsObject = JSObject.fromJSONObject(jsonObject) + } catch (_: JSONException) { + } + newAttachment.id = jsObject!!.getString("id") + newAttachment.url = jsObject.getString("url") + try { + newAttachment.options = jsObject.getJSONObject("options") + } catch (_: JSONException) { + } + attachmentsList.add(newAttachment) + } + } + } + return attachmentsList + } + } +} \ No newline at end of file diff --git a/plugins/notification/android/src/main/java/NotificationPlugin.kt b/plugins/notification/android/src/main/java/NotificationPlugin.kt new file mode 100644 index 0000000..19a8283 --- /dev/null +++ b/plugins/notification/android/src/main/java/NotificationPlugin.kt @@ -0,0 +1,286 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +package app.tauri.notification + +import android.Manifest +import android.annotation.SuppressLint +import android.app.Activity +import android.app.NotificationManager +import android.content.Context +import android.content.Intent +import android.os.Build +import android.webkit.WebView +import app.tauri.PermissionState +import app.tauri.annotation.Command +import app.tauri.annotation.InvokeArg +import app.tauri.annotation.Permission +import app.tauri.annotation.PermissionCallback +import app.tauri.annotation.TauriPlugin +import app.tauri.plugin.Invoke +import app.tauri.plugin.JSArray +import app.tauri.plugin.JSObject +import app.tauri.plugin.Plugin + +const val LOCAL_NOTIFICATIONS = "permissionState" + +@InvokeArg +class PluginConfig { + var icon: String? = null + var sound: String? = null + var iconColor: String? = null +} + +@InvokeArg +class BatchArgs { + lateinit var notifications: List +} + +@InvokeArg +class CancelArgs { + // `cancelAll()` sends no list: cancel every pending notification + var notifications: List? = null +} + +@InvokeArg +class NotificationAction { + lateinit var id: String + var title: String? = null + var input: Boolean? = null +} + +@InvokeArg +class ActionType { + lateinit var id: String + lateinit var actions: List +} + +@InvokeArg +class RegisterActionTypesArgs { + lateinit var types: List +} + +@InvokeArg +class ActiveNotification { + var id: Int = 0 + var tag: String? = null +} + +@InvokeArg +class RemoveActiveArgs { + var notifications: List = listOf() +} + +@TauriPlugin( + permissions = [ + Permission(strings = [Manifest.permission.POST_NOTIFICATIONS], alias = "permissionState") + ] +) +class NotificationPlugin(private val activity: Activity): Plugin(activity) { + private var webView: WebView? = null + private lateinit var manager: TauriNotificationManager + private lateinit var notificationManager: NotificationManager + private lateinit var notificationStorage: NotificationStorage + private var channelManager = ChannelManager(activity) + + companion object { + var instance: NotificationPlugin? = null + + fun triggerNotification(notification: Notification) { + instance?.triggerObject("notification", notification) + } + } + + override fun load(webView: WebView) { + instance = this + + super.load(webView) + this.webView = webView + notificationStorage = NotificationStorage(activity, jsonMapper()) + + val manager = TauriNotificationManager( + notificationStorage, + activity, + activity, + getConfig(PluginConfig::class.java) + ) + manager.createNotificationChannel() + + this.manager = manager + + notificationManager = activity.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager + + val intent = activity.intent + intent?.let { + onIntent(it) + } + } + + override fun onNewIntent(intent: Intent) { + super.onNewIntent(intent) + onIntent(intent) + } + + fun onIntent(intent: Intent) { + if (Intent.ACTION_MAIN != intent.action) { + return + } + val dataJson = manager.handleNotificationActionPerformed(intent, notificationStorage) + if (dataJson != null) { + trigger("actionPerformed", dataJson) + } + } + + @Command + fun show(invoke: Invoke) { + val notification = invoke.parseArgs(Notification::class.java) + val id = manager.schedule(notification) + + invoke.resolveObject(id) + } + + @Command + fun batch(invoke: Invoke) { + val args = invoke.parseArgs(BatchArgs::class.java) + + val ids = manager.schedule(args.notifications) + notificationStorage.appendNotifications(args.notifications) + + invoke.resolveObject(ids) + } + + @Command + fun cancel(invoke: Invoke) { + val args = invoke.parseArgs(CancelArgs::class.java) + manager.cancel( + args.notifications ?: notificationStorage.getSavedNotificationIds().mapNotNull { it.toIntOrNull() } + ) + invoke.resolve() + } + + @Command + fun removeActive(invoke: Invoke) { + val args = invoke.parseArgs(RemoveActiveArgs::class.java) + + if (args.notifications.isEmpty()) { + notificationManager.cancelAll() + invoke.resolve() + } else { + for (notification in args.notifications) { + if (notification.tag == null) { + notificationManager.cancel(notification.id) + } else { + notificationManager.cancel(notification.tag, notification.id) + } + } + invoke.resolve() + } + } + + @Command + fun getPending(invoke: Invoke) { + val notifications= notificationStorage.getSavedNotifications() + val result = Notification.buildNotificationPendingList(notifications) + invoke.resolveObject(result) + } + + @Command + fun registerActionTypes(invoke: Invoke) { + val args = invoke.parseArgs(RegisterActionTypesArgs::class.java) + notificationStorage.writeActionGroup(args.types) + invoke.resolve() + } + + @SuppressLint("ObsoleteSdkInt") + @Command + fun getActive(invoke: Invoke) { + // plain lists and maps: until tauri serializes org.json values natively, + // `resolveObject` turns a `JSArray` into `{ "values": [...] }` + val notifications = mutableListOf>() + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + val activeNotifications = notificationManager.activeNotifications + for (activeNotification in activeNotifications) { + val jsNotification = mutableMapOf( + "id" to activeNotification.id, + "tag" to activeNotification.tag + ) + val notification = activeNotification.notification + if (notification != null) { + jsNotification["title"] = notification.extras.getCharSequence(android.app.Notification.EXTRA_TITLE)?.toString() + jsNotification["body"] = notification.extras.getCharSequence(android.app.Notification.EXTRA_TEXT)?.toString() + jsNotification["group"] = notification.group + jsNotification["groupSummary"] = + 0 != notification.flags and android.app.Notification.FLAG_GROUP_SUMMARY + val extras = mutableMapOf() + for (key in notification.extras.keySet()) { + extras[key] = notification.extras.getString(key) + } + jsNotification["data"] = extras + } + notifications.add(jsNotification) + } + } + + invoke.resolveObject(notifications) + } + + @Command + fun createChannel(invoke: Invoke) { + channelManager.createChannel(invoke) + } + + @Command + fun deleteChannel(invoke: Invoke) { + channelManager.deleteChannel(invoke) + } + + @Command + fun listChannels(invoke: Invoke) { + channelManager.listChannels(invoke) + } + + @Command + override fun checkPermissions(invoke: Invoke) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) { + val permissionsResultJSON = JSObject() + permissionsResultJSON.put("permissionState", getPermissionState()) + invoke.resolve(permissionsResultJSON) + } else { + super.checkPermissions(invoke) + } + } + + @Command + override fun requestPermissions(invoke: Invoke) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) { + permissionState(invoke) + } else { + if (getPermissionState(LOCAL_NOTIFICATIONS) !== PermissionState.GRANTED) { + requestPermissionForAlias(LOCAL_NOTIFICATIONS, invoke, "permissionsCallback") + } + } + } + + @Command + fun permissionState(invoke: Invoke) { + val permissionsResultJSON = JSObject() + permissionsResultJSON.put("permissionState", getPermissionState()) + invoke.resolve(permissionsResultJSON) + } + + @PermissionCallback + private fun permissionsCallback(invoke: Invoke) { + val permissionsResultJSON = JSObject() + permissionsResultJSON.put("permissionState", getPermissionState()) + invoke.resolve(permissionsResultJSON) + } + + private fun getPermissionState(): String { + return if (manager.areNotificationsEnabled()) { + "granted" + } else { + "denied" + } + } +} diff --git a/plugins/notification/android/src/main/java/NotificationSchedule.kt b/plugins/notification/android/src/main/java/NotificationSchedule.kt new file mode 100644 index 0000000..316ff90 --- /dev/null +++ b/plugins/notification/android/src/main/java/NotificationSchedule.kt @@ -0,0 +1,369 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +package app.tauri.notification + +import android.annotation.SuppressLint +import android.text.format.DateUtils +import com.fasterxml.jackson.annotation.JsonFormat +import com.fasterxml.jackson.annotation.JsonProperty +import com.fasterxml.jackson.core.JsonGenerator +import com.fasterxml.jackson.core.JsonParser +import com.fasterxml.jackson.core.JsonProcessingException +import com.fasterxml.jackson.databind.DeserializationContext +import com.fasterxml.jackson.databind.JsonDeserializer +import com.fasterxml.jackson.databind.JsonNode +import com.fasterxml.jackson.databind.SerializerProvider +import com.fasterxml.jackson.databind.annotation.JsonDeserialize +import com.fasterxml.jackson.databind.annotation.JsonSerialize +import com.fasterxml.jackson.databind.deser.std.StdDeserializer +import com.fasterxml.jackson.databind.ser.std.StdSerializer +import java.io.IOException +import java.text.SimpleDateFormat +import java.util.Calendar +import java.util.Date +import java.util.TimeZone + +const val JS_DATE_FORMAT = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'" + +enum class NotificationInterval { + @JsonProperty("year") + Year, + @JsonProperty("month") + Month, + @JsonProperty("twoWeeks") + TwoWeeks, + @JsonProperty("week") + Week, + @JsonProperty("day") + Day, + @JsonProperty("hour") + Hour, + @JsonProperty("minute") + Minute, + @JsonProperty("second") + Second +} + +fun getIntervalTime(interval: NotificationInterval, count: Int): Long { + return when (interval) { + // This case is just approximation as not all years have the same number of days + NotificationInterval.Year -> count * DateUtils.WEEK_IN_MILLIS * 52 + // This case is just approximation as months have different number of days + NotificationInterval.Month -> count * 30 * DateUtils.DAY_IN_MILLIS + NotificationInterval.TwoWeeks -> count * 2 * DateUtils.WEEK_IN_MILLIS + NotificationInterval.Week -> count * DateUtils.WEEK_IN_MILLIS + NotificationInterval.Day -> count * DateUtils.DAY_IN_MILLIS + NotificationInterval.Hour -> count * DateUtils.HOUR_IN_MILLIS + NotificationInterval.Minute -> count * DateUtils.MINUTE_IN_MILLIS + NotificationInterval.Second -> count * DateUtils.SECOND_IN_MILLIS + } +} + +@JsonDeserialize(using = NotificationScheduleDeserializer::class) +@JsonSerialize(using = NotificationScheduleSerializer::class) +sealed class NotificationSchedule { + // At specific moment of time (with repeating option) + @JsonDeserialize + class At: NotificationSchedule() { + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = JS_DATE_FORMAT) + lateinit var date: Date + var repeating: Boolean = false + var allowWhileIdle: Boolean = false + } + @JsonDeserialize + class Interval: NotificationSchedule() { + lateinit var interval: DateMatch + var allowWhileIdle: Boolean = false + } + @JsonDeserialize + class Every: NotificationSchedule() { + lateinit var interval: NotificationInterval + var count: Int = 0 + var allowWhileIdle: Boolean = false + } + + fun isRemovable(): Boolean { + return when (this) { + is At -> !repeating + else -> false + } + } + + fun allowWhileIdle(): Boolean { + return when (this) { + is At -> allowWhileIdle + is Interval -> allowWhileIdle + is Every -> allowWhileIdle + else -> false + } + } +} + +internal class NotificationScheduleSerializer @JvmOverloads constructor(t: Class? = null) : + StdSerializer(t) { + @SuppressLint("SimpleDateFormat") + @Throws(IOException::class, JsonProcessingException::class) + override fun serialize( + value: NotificationSchedule, jgen: JsonGenerator, provider: SerializerProvider + ) { + jgen.writeStartObject() + when (value) { + is NotificationSchedule.At -> { + jgen.writeObjectFieldStart("at") + + val sdf = SimpleDateFormat(JS_DATE_FORMAT) + sdf.timeZone = TimeZone.getTimeZone("UTC") + jgen.writeStringField("date", sdf.format(value.date)) + jgen.writeBooleanField("repeating", value.repeating) + + jgen.writeEndObject() + } + is NotificationSchedule.Interval -> { + jgen.writeObjectFieldStart("interval") + + jgen.writeObjectField("interval", value.interval) + + jgen.writeEndObject() + } + is NotificationSchedule.Every -> { + jgen.writeObjectFieldStart("every") + + jgen.writeObjectField("interval", value.interval) + jgen.writeNumberField("count", value.count) + + jgen.writeEndObject() + } + } + + jgen.writeEndObject() + } +} + +internal class NotificationScheduleDeserializer: JsonDeserializer() { + override fun deserialize( + jsonParser: JsonParser, + deserializationContext: DeserializationContext + ): NotificationSchedule { + val node: JsonNode = jsonParser.codec.readTree(jsonParser) + node.get("at")?.let { + return jsonParser.codec.treeToValue(it, NotificationSchedule.At::class.java) + } + node.get("interval")?.let { + return jsonParser.codec.treeToValue(it, NotificationSchedule.Interval::class.java) + } + node.get("every")?.let { + return jsonParser.codec.treeToValue(it, NotificationSchedule.Every::class.java) + } + throw Error("unknown schedule kind $node") + } +} + +class DateMatch { + var year: Int? = null + var month: Int? = null + var day: Int? = null + var weekday: Int? = null + var hour: Int? = null + var minute: Int? = null + var second: Int? = null + + // Unit used to save the last used unit for a trigger. + // One of the Calendar constants values + var unit: Int? = -1 + + /** + * Gets a calendar instance pointing to the specified date. + * + * @param date The date to point. + */ + private fun buildCalendar(date: Date): Calendar { + val cal: Calendar = Calendar.getInstance() + cal.time = date + cal.set(Calendar.MILLISECOND, 0) + return cal + } + + /** + * Calculates next trigger date for + * + * @param date base date used to calculate trigger + * @return next trigger timestamp + */ + fun nextTrigger(date: Date): Long { + val current: Calendar = buildCalendar(date) + val next: Calendar = buildNextTriggerTime(date) + return postponeTriggerIfNeeded(current, next) + } + + /** + * Postpone trigger if first schedule matches the past + */ + private fun postponeTriggerIfNeeded(current: Calendar, next: Calendar): Long { + if (next.timeInMillis <= current.timeInMillis && unit != -1) { + var incrementUnit = -1 + if (unit == Calendar.YEAR || unit == Calendar.MONTH) { + incrementUnit = Calendar.YEAR + } else if (unit == Calendar.DAY_OF_MONTH) { + incrementUnit = Calendar.MONTH + } else if (unit == Calendar.DAY_OF_WEEK) { + incrementUnit = Calendar.WEEK_OF_MONTH + } else if (unit == Calendar.HOUR_OF_DAY) { + incrementUnit = Calendar.DAY_OF_MONTH + } else if (unit == Calendar.MINUTE) { + incrementUnit = Calendar.HOUR_OF_DAY + } else if (unit == Calendar.SECOND) { + incrementUnit = Calendar.MINUTE + } + if (incrementUnit != -1) { + next.set(incrementUnit, next.get(incrementUnit) + 1) + } + } + return next.timeInMillis + } + + private fun buildNextTriggerTime(date: Date): Calendar { + val next: Calendar = buildCalendar(date) + if (year != null) { + next.set(Calendar.YEAR, year ?: 0) + if (unit == -1) unit = Calendar.YEAR + } + if (month != null) { + next.set(Calendar.MONTH, month ?: 0) + if (unit == -1) unit = Calendar.MONTH + } + if (day != null) { + next.set(Calendar.DAY_OF_MONTH, day ?: 0) + if (unit == -1) unit = Calendar.DAY_OF_MONTH + } + if (weekday != null) { + next.set(Calendar.DAY_OF_WEEK, weekday ?: 0) + if (unit == -1) unit = Calendar.DAY_OF_WEEK + } + if (hour != null) { + next.set(Calendar.HOUR_OF_DAY, hour ?: 0) + if (unit == -1) unit = Calendar.HOUR_OF_DAY + } + if (minute != null) { + next.set(Calendar.MINUTE, minute ?: 0) + if (unit == -1) unit = Calendar.MINUTE + } + if (second != null) { + next.set(Calendar.SECOND, second ?: 0) + if (unit == -1) unit = Calendar.SECOND + } + return next + } + + override fun toString(): String { + return "DateMatch{" + + "year=" + + year + + ", month=" + + month + + ", day=" + + day + + ", weekday=" + + weekday + + ", hour=" + + hour + + ", minute=" + + minute + + ", second=" + + second + + '}' + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other == null || javaClass != other.javaClass) return false + val dateMatch = other as DateMatch + if (if (year != null) year != dateMatch.year else dateMatch.year != null) return false + if (if (month != null) month != dateMatch.month else dateMatch.month != null) return false + if (if (day != null) day != dateMatch.day else dateMatch.day != null) return false + if (if (weekday != null) weekday != dateMatch.weekday else dateMatch.weekday != null) return false + if (if (hour != null) hour != dateMatch.hour else dateMatch.hour != null) return false + if (if (minute != null) minute != dateMatch.minute else dateMatch.minute != null) return false + return if (second != null) second == dateMatch.second else dateMatch.second == null + } + + override fun hashCode(): Int { + var result = if (year != null) year.hashCode() else 0 + result = 31 * result + if (month != null) month.hashCode() else 0 + result = 31 * result + if (day != null) day.hashCode() else 0 + result = 31 * result + if (weekday != null) weekday.hashCode() else 0 + result = 31 * result + if (hour != null) hour.hashCode() else 0 + result = 31 * result + if (minute != null) minute.hashCode() else 0 + result += 31 + if (second != null) second.hashCode() else 0 + return result + } + + /** + * Transform DateMatch object to CronString + * + * @return + */ + fun toMatchString(): String { + val matchString = year.toString() + + separator + + month + + separator + + day + + separator + + weekday + + separator + + hour + + separator + + minute + + separator + + second + + separator + + unit + return matchString.replace("null", "*") + } + + companion object { + private const val separator = " " + + /** + * Create DateMatch object from stored string + * + * @param matchString + * @return + */ + fun fromMatchString(matchString: String): DateMatch { + val date = DateMatch() + val split = matchString.split(separator.toRegex()).dropLastWhile { it.isEmpty() } + .toTypedArray() + if (split.size == 7) { + date.year = getValueFromCronElement(split[0]) + date.month = getValueFromCronElement(split[1]) + date.day = getValueFromCronElement(split[2]) + date.weekday = getValueFromCronElement(split[3]) + date.hour = getValueFromCronElement(split[4]) + date.minute = getValueFromCronElement(split[5]) + date.unit = getValueFromCronElement(split[6]) + } + if (split.size == 8) { + date.year = getValueFromCronElement(split[0]) + date.month = getValueFromCronElement(split[1]) + date.day = getValueFromCronElement(split[2]) + date.weekday = getValueFromCronElement(split[3]) + date.hour = getValueFromCronElement(split[4]) + date.minute = getValueFromCronElement(split[5]) + date.second = getValueFromCronElement(split[6]) + date.unit = getValueFromCronElement(split[7]) + } + return date + } + + private fun getValueFromCronElement(token: String): Int? { + return try { + token.toInt() + } catch (e: NumberFormatException) { + null + } + } + } +} \ No newline at end of file diff --git a/plugins/notification/android/src/main/java/NotificationStorage.kt b/plugins/notification/android/src/main/java/NotificationStorage.kt new file mode 100644 index 0000000..d4cb969 --- /dev/null +++ b/plugins/notification/android/src/main/java/NotificationStorage.kt @@ -0,0 +1,113 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +package app.tauri.notification + +import android.content.Context +import android.content.SharedPreferences +import com.fasterxml.jackson.databind.ObjectMapper +import org.json.JSONException +import java.lang.Exception + +// Key for private preferences +private const val NOTIFICATION_STORE_ID = "NOTIFICATION_STORE" +// Key used to save action types +private const val ACTION_TYPES_ID = "ACTION_TYPE_STORE" + +class NotificationStorage(private val context: Context, private val jsonMapper: ObjectMapper) { + fun appendNotifications(localNotifications: List) { + val storage = getStorage(NOTIFICATION_STORE_ID) + val editor = storage.edit() + for (request in localNotifications) { + if (request.schedule != null) { + val key: String = request.id.toString() + editor.putString(key, request.sourceJson.toString()) + } + } + editor.apply() + } + + fun getSavedNotificationIds(): List { + val storage = getStorage(NOTIFICATION_STORE_ID) + val all = storage.all + return if (all != null) { + ArrayList(all.keys) + } else ArrayList() + } + + fun getSavedNotifications(): List { + val storage = getStorage(NOTIFICATION_STORE_ID) + val all = storage.all + if (all != null) { + val notifications = ArrayList() + for (key in all.keys) { + val notificationString = all[key] as String? + try { + val notification = jsonMapper.readValue(notificationString, Notification::class.java) + notifications.add(notification) + } catch (_: Exception) { } + } + return notifications + } + return ArrayList() + } + + fun getSavedNotification(key: String): Notification? { + val storage = getStorage(NOTIFICATION_STORE_ID) + val notificationString = try { + storage.getString(key, null) + } catch (ex: ClassCastException) { + return null + } ?: return null + + return try { + jsonMapper.readValue(notificationString, Notification::class.java) + } catch (ex: JSONException) { + null + } + } + + fun deleteNotification(id: String?) { + val editor = getStorage(NOTIFICATION_STORE_ID).edit() + editor.remove(id) + editor.apply() + } + + private fun getStorage(key: String): SharedPreferences { + return context.getSharedPreferences(key, Context.MODE_PRIVATE) + } + + fun writeActionGroup(actions: List) { + for (type in actions) { + val editor = getStorage(ACTION_TYPES_ID + type.id).edit() + editor.clear() + editor.putInt("count", type.actions.size) + for (i in 0 until type.actions.size) { + val action = type.actions[i] + editor.putString("id$i", action.id) + editor.putString("title$i", action.title) + editor.putBoolean("input$i", action.input ?: false) + } + editor.apply() + } + } + + fun getActionGroup(forId: String): Array { + val storage = getStorage(ACTION_TYPES_ID + forId) + val count = storage.getInt("count", 0) + val actions: Array = arrayOfNulls(count) + for (i in 0 until count) { + val id = storage.getString("id$i", "") + val title = storage.getString("title$i", "") + val input = storage.getBoolean("input$i", false) + + val action = NotificationAction() + action.id = id ?: "" + action.title = title + action.input = input + actions[i] = action + } + return actions + } +} \ No newline at end of file diff --git a/plugins/notification/android/src/main/java/TauriNotificationManager.kt b/plugins/notification/android/src/main/java/TauriNotificationManager.kt new file mode 100644 index 0000000..a891273 --- /dev/null +++ b/plugins/notification/android/src/main/java/TauriNotificationManager.kt @@ -0,0 +1,581 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +package app.tauri.notification + +import android.annotation.SuppressLint +import android.app.Activity +import android.app.AlarmManager +import android.app.NotificationChannel +import android.app.NotificationManager +import android.app.PendingIntent +import android.content.BroadcastReceiver +import android.content.ContentResolver +import android.content.Context +import android.content.Intent +import android.graphics.Color +import android.media.AudioAttributes +import android.net.Uri +import android.os.Build +import android.os.Build.VERSION.SDK_INT +import android.os.UserManager +import androidx.core.app.NotificationCompat +import androidx.core.app.NotificationManagerCompat +import androidx.core.app.RemoteInput +import app.tauri.Logger +import app.tauri.plugin.JSObject +import app.tauri.plugin.PluginManager +import com.fasterxml.jackson.databind.ObjectMapper +import org.json.JSONException +import org.json.JSONObject +import java.text.SimpleDateFormat +import java.util.Date + +// Action constants +const val NOTIFICATION_INTENT_KEY = "NotificationId" +const val NOTIFICATION_OBJ_INTENT_KEY = "LocalNotficationObject" +const val ACTION_INTENT_KEY = "NotificationUserAction" +const val NOTIFICATION_IS_REMOVABLE_KEY = "NotificationRepeating" +const val REMOTE_INPUT_KEY = "NotificationRemoteInput" +const val DEFAULT_NOTIFICATION_CHANNEL_ID = "default" +const val DEFAULT_PRESS_ACTION = "tap" + +class TauriNotificationManager( + private val storage: NotificationStorage, + private val activity: Activity?, + private val context: Context, + private val config: PluginConfig? +) { + private var defaultSoundID: Int = AssetUtils.RESOURCE_ID_ZERO_VALUE + private var defaultSmallIconID: Int = AssetUtils.RESOURCE_ID_ZERO_VALUE + + fun handleNotificationActionPerformed( + data: Intent, + notificationStorage: NotificationStorage + ): JSObject? { + Logger.debug(Logger.tags("Notification"), "Notification received: " + data.dataString) + val notificationId = + data.getIntExtra(NOTIFICATION_INTENT_KEY, Int.MIN_VALUE) + if (notificationId == Int.MIN_VALUE) { + Logger.debug(Logger.tags("Notification"), "Activity started without notification attached") + return null + } + val isRemovable = + data.getBooleanExtra(NOTIFICATION_IS_REMOVABLE_KEY, true) + if (isRemovable) { + notificationStorage.deleteNotification(notificationId.toString()) + } + val dataJson = JSObject() + val results = RemoteInput.getResultsFromIntent(data) + val input = results?.getCharSequence(REMOTE_INPUT_KEY) + dataJson.put("inputValue", input?.toString()) + val menuAction = data.getStringExtra(ACTION_INTENT_KEY) + dismissVisibleNotification(notificationId) + dataJson.put("actionId", menuAction) + var request: JSONObject? = null + try { + val notificationJsonString = + data.getStringExtra(NOTIFICATION_OBJ_INTENT_KEY) + if (notificationJsonString != null) { + request = JSObject(notificationJsonString) + } + } catch (_: JSONException) { + } + dataJson.put("notification", request) + return dataJson + } + + /** + * Create notification channel + */ + fun createNotificationChannel() { + // Create the NotificationChannel, but only on API 26+ because + // the NotificationChannel class is new and not in the support library + if (SDK_INT >= Build.VERSION_CODES.O) { + val name: CharSequence = "Default" + val description = "Default" + val importance = NotificationManager.IMPORTANCE_DEFAULT + val channel = NotificationChannel(DEFAULT_NOTIFICATION_CHANNEL_ID, name, importance) + channel.description = description + val audioAttributes = AudioAttributes.Builder() + .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION) + .setUsage(AudioAttributes.USAGE_ALARM) + .build() + val soundUri = getDefaultSoundUrl(context) + if (soundUri != null) { + channel.setSound(soundUri, audioAttributes) + } + // Register the channel with the system; you can't change the importance + // or other notification behaviors after this + val notificationManager = context.getSystemService( + NotificationManager::class.java + ) + notificationManager.createNotificationChannel(channel) + } + } + + private fun trigger(notificationManager: NotificationManagerCompat, notification: Notification): Int { + dismissVisibleNotification(notification.id) + cancelTimerForNotification(notification.id) + buildNotification(notificationManager, notification) + + return notification.id + } + + fun schedule(notification: Notification): Int { + val notificationManager = NotificationManagerCompat.from(context) + return trigger(notificationManager, notification) + } + + fun schedule(notifications: List): List { + val ids = mutableListOf() + val notificationManager = NotificationManagerCompat.from(context) + + for (notification in notifications) { + val id = trigger(notificationManager, notification) + ids.add(id) + } + + return ids + } + + // TODO Progressbar support + // TODO System categories (DO_NOT_DISTURB etc.) + // TODO use NotificationCompat.MessagingStyle for latest API + // TODO expandable notification NotificationCompat.MessagingStyle + // TODO media style notification support NotificationCompat.MediaStyle + @SuppressLint("MissingPermission") + private fun buildNotification( + notificationManager: NotificationManagerCompat, + notification: Notification, + ) { + val channelId = notification.channelId ?: DEFAULT_NOTIFICATION_CHANNEL_ID + val mBuilder = NotificationCompat.Builder( + context, channelId + ) + .setContentTitle(notification.title) + .setContentText(notification.body) + .setAutoCancel(notification.isAutoCancel) + .setOngoing(notification.isOngoing) + .setPriority(NotificationCompat.PRIORITY_DEFAULT) + .setGroupSummary(notification.isGroupSummary) + if (notification.largeBody != null) { + // support multiline text + mBuilder.setStyle( + NotificationCompat.BigTextStyle() + .bigText(notification.largeBody) + .setSummaryText(notification.summary) + ) + } else if (notification.inboxLines != null) { + val inboxStyle = NotificationCompat.InboxStyle() + for (line in notification.inboxLines ?: listOf()) { + inboxStyle.addLine(line) + } + inboxStyle.setBigContentTitle(notification.title) + inboxStyle.setSummaryText(notification.summary) + mBuilder.setStyle(inboxStyle) + } + val sound = notification.getSound(context, getDefaultSound(context)) + if (sound != null) { + val soundUri = Uri.parse(sound) + // Grant permission to use sound + context.grantUriPermission( + "com.android.systemui", + soundUri, + Intent.FLAG_GRANT_READ_URI_PERMISSION + ) + mBuilder.setSound(soundUri) + mBuilder.setDefaults(android.app.Notification.DEFAULT_VIBRATE or android.app.Notification.DEFAULT_LIGHTS) + } else { + mBuilder.setDefaults(android.app.Notification.DEFAULT_ALL) + } + val group = notification.group + if (group != null) { + mBuilder.setGroup(group) + if (notification.isGroupSummary) { + mBuilder.setSubText(notification.summary) + } + } + mBuilder.setVisibility(notification.visibility ?: NotificationCompat.VISIBILITY_PRIVATE) + mBuilder.setOnlyAlertOnce(true) + mBuilder.setSmallIcon(notification.getSmallIcon(context, getDefaultSmallIcon(context))) + mBuilder.setLargeIcon(notification.getLargeIcon(context)) + val iconColor = notification.getIconColor(config?.iconColor ?: "") + if (iconColor.isNotEmpty()) { + try { + mBuilder.color = Color.parseColor(iconColor) + } catch (ex: IllegalArgumentException) { + throw Exception("Invalid color provided. Must be a hex string (ex: #ff0000") + } + } + createActionIntents(notification, mBuilder) + // notificationId is a unique int for each notification that you must define + val buildNotification = mBuilder.build() + if (notification.schedule != null) { + triggerScheduledNotification(buildNotification, notification) + } else { + notificationManager.notify(notification.id, buildNotification) + try { + NotificationPlugin.triggerNotification(notification) + } catch (_: JSONException) { + } + } + } + + // Create intents for open/dismiss actions + private fun createActionIntents( + notification: Notification, + mBuilder: NotificationCompat.Builder + ) { + // Open intent + val intent = buildIntent(notification, DEFAULT_PRESS_ACTION) + var flags = PendingIntent.FLAG_CANCEL_CURRENT + if (SDK_INT >= Build.VERSION_CODES.S) { + flags = flags or PendingIntent.FLAG_MUTABLE + } + val pendingIntent = PendingIntent.getActivity(context, notification.id, intent, flags) + mBuilder.setContentIntent(pendingIntent) + + // Build action types + val actionTypeId = notification.actionTypeId + if (actionTypeId != null) { + val actionGroup = storage.getActionGroup(actionTypeId) + for (notificationAction in actionGroup) { + // TODO Add custom icons to actions + val actionIntent = buildIntent(notification, notificationAction!!.id) + val actionPendingIntent = PendingIntent.getActivity( + context, + (notification.id) + notificationAction.id.hashCode(), + actionIntent, + flags + ) + val actionBuilder: NotificationCompat.Action.Builder = NotificationCompat.Action.Builder( + R.drawable.ic_transparent, + notificationAction.title, + actionPendingIntent + ) + if (notificationAction.input == true) { + val remoteInput = RemoteInput.Builder(REMOTE_INPUT_KEY).setLabel( + notificationAction.title + ).build() + actionBuilder.addRemoteInput(remoteInput) + } + mBuilder.addAction(actionBuilder.build()) + } + } + + // Dismiss intent + val dissmissIntent = Intent( + context, + NotificationDismissReceiver::class.java + ) + dissmissIntent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK + dissmissIntent.putExtra(NOTIFICATION_INTENT_KEY, notification.id) + dissmissIntent.putExtra(ACTION_INTENT_KEY, "dismiss") + val schedule = notification.schedule + dissmissIntent.putExtra( + NOTIFICATION_IS_REMOVABLE_KEY, + schedule == null || schedule.isRemovable() + ) + flags = 0 + if (SDK_INT >= Build.VERSION_CODES.S) { + flags = PendingIntent.FLAG_MUTABLE + } + val deleteIntent = + PendingIntent.getBroadcast(context, notification.id, dissmissIntent, flags) + mBuilder.setDeleteIntent(deleteIntent) + } + + private fun buildIntent(notification: Notification, action: String?): Intent { + val intent = if (activity != null) { + Intent(context, activity.javaClass) + } else { + val packageName = context.packageName + context.packageManager.getLaunchIntentForPackage(packageName)!! + } + intent.action = Intent.ACTION_MAIN + intent.addCategory(Intent.CATEGORY_LAUNCHER) + intent.flags = Intent.FLAG_ACTIVITY_SINGLE_TOP or Intent.FLAG_ACTIVITY_CLEAR_TOP + intent.putExtra(NOTIFICATION_INTENT_KEY, notification.id) + intent.putExtra(ACTION_INTENT_KEY, action) + intent.putExtra(NOTIFICATION_OBJ_INTENT_KEY, notification.sourceJson) + val schedule = notification.schedule + intent.putExtra(NOTIFICATION_IS_REMOVABLE_KEY, schedule == null || schedule.isRemovable()) + return intent + } + + /** + * Build a notification trigger, such as triggering each N seconds, or + * on a certain date "shape" (such as every first of the month) + */ + // TODO support different AlarmManager.RTC modes depending on priority + @SuppressLint("SimpleDateFormat") + private fun triggerScheduledNotification(notification: android.app.Notification, request: Notification) { + val alarmManager = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager + val schedule = request.schedule + val notificationIntent = Intent( + context, + TimedNotificationPublisher::class.java + ) + notificationIntent.putExtra(NOTIFICATION_INTENT_KEY, request.id) + notificationIntent.putExtra(TimedNotificationPublisher.NOTIFICATION_KEY, notification) + var flags = PendingIntent.FLAG_CANCEL_CURRENT + if (SDK_INT >= Build.VERSION_CODES.S) { + flags = flags or PendingIntent.FLAG_MUTABLE + } + var pendingIntent = + PendingIntent.getBroadcast(context, request.id, notificationIntent, flags) + + when (schedule) { + is NotificationSchedule.At -> { + if (schedule.date.time < Date().time) { + Logger.error(Logger.tags("Notification"), "Scheduled time must be *after* current time", null) + return + } + if (schedule.repeating) { + val interval: Long = schedule.date.time - Date().time + alarmManager.setRepeating(AlarmManager.RTC, schedule.date.time, interval, pendingIntent) + } else { + setExactIfPossible(alarmManager, schedule, schedule.date.time, pendingIntent) + } + } + is NotificationSchedule.Interval -> { + val trigger = schedule.interval.nextTrigger(Date()) + notificationIntent.putExtra(TimedNotificationPublisher.CRON_KEY, schedule.interval.toMatchString()) + pendingIntent = + PendingIntent.getBroadcast(context, request.id, notificationIntent, flags) + setExactIfPossible(alarmManager, schedule, trigger, pendingIntent) + val sdf = SimpleDateFormat("yyyy/MM/dd HH:mm:ss") + Logger.debug( + Logger.tags("Notification"), + "notification " + request.id + " will next fire at " + sdf.format(Date(trigger)) + ) + } + is NotificationSchedule.Every -> { + val everyInterval = getIntervalTime(schedule.interval, schedule.count) + val startTime: Long = Date().time + everyInterval + alarmManager.setRepeating(AlarmManager.RTC, startTime, everyInterval, pendingIntent) + } + else -> {} + } + } + + @SuppressLint("ObsoleteSdkInt", "MissingPermission") + private fun setExactIfPossible( + alarmManager: AlarmManager, + schedule: NotificationSchedule, + trigger: Long, + pendingIntent: PendingIntent + ) { + if (SDK_INT >= Build.VERSION_CODES.S && !alarmManager.canScheduleExactAlarms()) { + if (SDK_INT >= Build.VERSION_CODES.M && schedule.allowWhileIdle()) { + alarmManager.setAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, trigger, pendingIntent) + } else { + alarmManager[AlarmManager.RTC, trigger] = pendingIntent + } + } else { + if (SDK_INT >= Build.VERSION_CODES.M && schedule.allowWhileIdle()) { + alarmManager.setExactAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, trigger, pendingIntent) + } else { + alarmManager.setExact(AlarmManager.RTC, trigger, pendingIntent) + } + } + } + + fun cancel(notifications: List) { + for (id in notifications) { + dismissVisibleNotification(id) + cancelTimerForNotification(id) + storage.deleteNotification(id.toString()) + } + } + + private fun cancelTimerForNotification(notificationId: Int) { + val intent = Intent(context, TimedNotificationPublisher::class.java) + var flags = 0 + if (SDK_INT >= Build.VERSION_CODES.S) { + flags = PendingIntent.FLAG_MUTABLE + } + val pi = PendingIntent.getBroadcast(context, notificationId, intent, flags) + if (pi != null) { + val alarmManager = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager + alarmManager.cancel(pi) + } + } + + private fun dismissVisibleNotification(notificationId: Int) { + val notificationManager = NotificationManagerCompat.from( + context + ) + notificationManager.cancel(notificationId) + } + + fun areNotificationsEnabled(): Boolean { + val notificationManager = NotificationManagerCompat.from(context) + return notificationManager.areNotificationsEnabled() + } + + private fun getDefaultSoundUrl(context: Context): Uri? { + val soundId = getDefaultSound(context) + return if (soundId != AssetUtils.RESOURCE_ID_ZERO_VALUE) { + Uri.parse(ContentResolver.SCHEME_ANDROID_RESOURCE + "://" + context.packageName + "/" + soundId) + } else null + } + + private fun getDefaultSound(context: Context): Int { + if (defaultSoundID != AssetUtils.RESOURCE_ID_ZERO_VALUE) return defaultSoundID + var resId: Int = AssetUtils.RESOURCE_ID_ZERO_VALUE + val soundConfigResourceName = AssetUtils.getResourceBaseName(config?.sound) + if (soundConfigResourceName != null) { + resId = AssetUtils.getResourceID(context, soundConfigResourceName, "raw") + } + defaultSoundID = resId + return resId + } + + private fun getDefaultSmallIcon(context: Context): Int { + if (defaultSmallIconID != AssetUtils.RESOURCE_ID_ZERO_VALUE) return defaultSmallIconID + var resId: Int = AssetUtils.RESOURCE_ID_ZERO_VALUE + val smallIconConfigResourceName = AssetUtils.getResourceBaseName(config?.icon) + if (smallIconConfigResourceName != null) { + resId = AssetUtils.getResourceID(context, smallIconConfigResourceName, "drawable") + } + if (resId == AssetUtils.RESOURCE_ID_ZERO_VALUE) { + resId = android.R.drawable.ic_dialog_info + } + defaultSmallIconID = resId + return resId + } +} + +class NotificationDismissReceiver : BroadcastReceiver() { + override fun onReceive(context: Context, intent: Intent) { + val intExtra = + intent.getIntExtra(NOTIFICATION_INTENT_KEY, Int.MIN_VALUE) + if (intExtra == Int.MIN_VALUE) { + Logger.error(Logger.tags("Notification"), "Invalid notification dismiss operation", null) + return + } + val isRemovable = + intent.getBooleanExtra(NOTIFICATION_IS_REMOVABLE_KEY, true) + if (isRemovable) { + val notificationStorage = NotificationStorage(context, ObjectMapper()) + notificationStorage.deleteNotification(intExtra.toString()) + } + } +} + +class TimedNotificationPublisher : BroadcastReceiver() { + /** + * Restore and present notification + */ + override fun onReceive(context: Context, intent: Intent) { + val notificationManager = + context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager + val notification = if (SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + intent.getParcelableExtra( + NOTIFICATION_KEY, + android.app.Notification::class.java + ) + } else { + getParcelableExtraLegacy(intent, NOTIFICATION_KEY) + } + notification?.`when` = System.currentTimeMillis() + val id = intent.getIntExtra(NOTIFICATION_INTENT_KEY, Int.MIN_VALUE) + if (id == Int.MIN_VALUE) { + Logger.error(Logger.tags("Notification"), "No valid id supplied", null) + } + val storage = NotificationStorage(context, ObjectMapper()) + + val savedNotification = storage.getSavedNotification(id.toString()) + if (savedNotification != null) { + NotificationPlugin.triggerNotification(savedNotification) + } + + notificationManager.notify(id, notification) + if (!rescheduleNotificationIfNeeded(context, intent, id)) { + storage.deleteNotification(id.toString()) + } + } + + @Suppress("DEPRECATION") + private fun getParcelableExtraLegacy(intent: Intent, string: String): android.app.Notification? { + return intent.getParcelableExtra(string) + } + + @SuppressLint("MissingPermission", "SimpleDateFormat") + private fun rescheduleNotificationIfNeeded(context: Context, intent: Intent, id: Int): Boolean { + val dateString = intent.getStringExtra(CRON_KEY) + if (dateString != null) { + val date = DateMatch.fromMatchString(dateString) + val alarmManager = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager + val trigger = date.nextTrigger(Date()) + val clone = intent.clone() as Intent + var flags = PendingIntent.FLAG_CANCEL_CURRENT + if (SDK_INT >= Build.VERSION_CODES.S) { + flags = flags or PendingIntent.FLAG_MUTABLE + } + val pendingIntent = PendingIntent.getBroadcast(context, id, clone, flags) + if (SDK_INT >= Build.VERSION_CODES.S && !alarmManager.canScheduleExactAlarms()) { + alarmManager[AlarmManager.RTC, trigger] = pendingIntent + } else { + alarmManager.setExact(AlarmManager.RTC, trigger, pendingIntent) + } + val sdf = SimpleDateFormat("yyyy/MM/dd HH:mm:ss") + Logger.debug( + Logger.tags("Notification"), + "notification " + id + " will next fire at " + sdf.format(Date(trigger)) + ) + return true + } + return false + } + + companion object { + var NOTIFICATION_KEY = "NotificationPublisher.notification" + var CRON_KEY = "NotificationPublisher.cron" + } +} + +class LocalNotificationRestoreReceiver : BroadcastReceiver() { + @SuppressLint("ObsoleteSdkInt") + override fun onReceive(context: Context, intent: Intent) { + if (SDK_INT >= Build.VERSION_CODES.N) { + val um = context.getSystemService( + UserManager::class.java + ) + if (um == null || !um.isUserUnlocked) return + } + val storage = NotificationStorage(context, ObjectMapper()) + val ids = storage.getSavedNotificationIds() + val notifications = mutableListOf() + val updatedNotifications = mutableListOf() + for (id in ids) { + val notification = storage.getSavedNotification(id) ?: continue + val schedule = notification.schedule + if (schedule != null && schedule is NotificationSchedule.At) { + val at: Date = schedule.date + if (at.before(Date())) { + // modify the scheduled date in order to show notifications that would have been delivered while device was off. + val newDateTime = Date().time + 15 * 1000 + schedule.date = Date(newDateTime) + updatedNotifications.add(notification) + } + } + notifications.add(notification) + } + if (updatedNotifications.size > 0) { + storage.appendNotifications(updatedNotifications) + } + + var config: PluginConfig? = null + try { + config = PluginManager.loadConfig(context, "notification", PluginConfig::class.java) + } catch (ex: Exception) { + ex.printStackTrace() + } + val notificationManager = TauriNotificationManager(storage, null, context, config) + notificationManager.schedule(notifications) + } +} diff --git a/plugins/notification/android/src/main/res/drawable/ic_transparent.xml b/plugins/notification/android/src/main/res/drawable/ic_transparent.xml new file mode 100644 index 0000000..fc1779e --- /dev/null +++ b/plugins/notification/android/src/main/res/drawable/ic_transparent.xml @@ -0,0 +1,12 @@ + + + + + + diff --git a/plugins/notification/android/src/test/java/ExampleUnitTest.kt b/plugins/notification/android/src/test/java/ExampleUnitTest.kt new file mode 100644 index 0000000..134a27d --- /dev/null +++ b/plugins/notification/android/src/test/java/ExampleUnitTest.kt @@ -0,0 +1,21 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +package app.tauri.notification + +import org.junit.Test + +import org.junit.Assert.* + +/** + * Example local unit test, which will execute on the development machine (host). + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +class ExampleUnitTest { + @Test + fun addition_isCorrect() { + assertEquals(4, 2 + 2) + } +} diff --git a/plugins/notification/api-iife.js b/plugins/notification/api-iife.js new file mode 100644 index 0000000..85f75b2 --- /dev/null +++ b/plugins/notification/api-iife.js @@ -0,0 +1 @@ +if("__TAURI__"in window){var __TAURI_PLUGIN_NOTIFICATION__=function(i,n){"use strict";var e,t,o;i.ScheduleEvery=void 0,(e=i.ScheduleEvery||(i.ScheduleEvery={})).Year="year",e.Month="month",e.TwoWeeks="twoWeeks",e.Week="week",e.Day="day",e.Hour="hour",e.Minute="minute",e.Second="second";return i.Importance=void 0,(t=i.Importance||(i.Importance={}))[t.None=0]="None",t[t.Min=1]="Min",t[t.Low=2]="Low",t[t.Default=3]="Default",t[t.High=4]="High",i.Visibility=void 0,(o=i.Visibility||(i.Visibility={}))[o.Secret=-1]="Secret",o[o.Private=0]="Private",o[o.Public=1]="Public",i.Schedule=class{static at(i,n=!1,e=!1){return{at:{date:i,repeating:n,allowWhileIdle:e},interval:void 0,every:void 0}}static interval(i,n=!1){return{at:void 0,interval:{interval:i,allowWhileIdle:n},every:void 0}}static every(i,n,e=!1){return{at:void 0,interval:void 0,every:{interval:i,count:n,allowWhileIdle:e}}}},i.active=async function(){return await n.invoke("plugin:notification|get_active")},i.cancel=async function(i){await n.invoke("plugin:notification|cancel",{notifications:i})},i.cancelAll=async function(){await n.invoke("plugin:notification|cancel")},i.channels=async function(){return await n.invoke("plugin:notification|list_channels")},i.createChannel=async function(i){await n.invoke("plugin:notification|create_channel",{...i})},i.isPermissionGranted=async function(){return"default"!==window.Notification.permission?await Promise.resolve("granted"===window.Notification.permission):await n.invoke("plugin:notification|is_permission_granted")},i.onAction=async function(i){return await n.addPluginListener("notification","actionPerformed",i)},i.onNotificationReceived=async function(i){return await n.addPluginListener("notification","notification",i)},i.pending=async function(){return await n.invoke("plugin:notification|get_pending")},i.registerActionTypes=async function(i){await n.invoke("plugin:notification|register_action_types",{types:i})},i.removeActive=async function(i){await n.invoke("plugin:notification|remove_active",{notifications:i})},i.removeAllActive=async function(){await n.invoke("plugin:notification|remove_active")},i.removeChannel=async function(i){await n.invoke("plugin:notification|delete_channel",{id:i})},i.requestPermission=async function(){return await window.Notification.requestPermission()},i.sendNotification=function(i){"string"==typeof i?new window.Notification(i):new window.Notification(i.title,i)},i}({},window.__TAURI__.core);Object.defineProperty(window.__TAURI__,"notification",{value:__TAURI_PLUGIN_NOTIFICATION__})} diff --git a/plugins/notification/banner.png b/plugins/notification/banner.png new file mode 100644 index 0000000000000000000000000000000000000000..f6460f3d07ce46cbc5bff0f54b7eb1c54998af24 GIT binary patch literal 60579 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
;=M+YK*8O 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�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 literal 0 HcmV?d00001 diff --git a/plugins/notification/build.rs b/plugins/notification/build.rs new file mode 100644 index 0000000..d7c0b3e --- /dev/null +++ b/plugins/notification/build.rs @@ -0,0 +1,36 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +const COMMANDS: &[&str] = &[ + "notify", + "request_permission", + "is_permission_granted", + "register_action_types", + "register_listener", + "remove_listener", + "cancel", + "get_pending", + "remove_active", + "get_active", + "check_permissions", + "show", + "batch", + "list_channels", + "delete_channel", + "create_channel", + "permission_state", +]; + +fn main() { + let result = tauri_plugin::Builder::new(COMMANDS) + .global_api_script_path("./api-iife.js") + .android_path("android") + .ios_path("ios") + .try_build(); + + // when building documentation for Android the plugin build result is always Err() and is irrelevant to the crate documentation build + if !(cfg!(docsrs) && std::env::var("TARGET").unwrap().contains("android")) { + result.unwrap(); + } +} diff --git a/plugins/notification/guest-js/index.ts b/plugins/notification/guest-js/index.ts new file mode 100644 index 0000000..534635f --- /dev/null +++ b/plugins/notification/guest-js/index.ts @@ -0,0 +1,967 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +/** + * Send toast notifications (brief auto-expiring OS window element) to your user. + * Can also be used with the Notification Web API. + * + * @module + */ + +import { + invoke, + type PluginListener, + addPluginListener +} from '@tauri-apps/api/core' + +export type { PermissionState } from '@tauri-apps/api/core' + +/** + * Options to send a notification. + * + * @since 2.0.0 + */ +interface Options { + /** + * The notification identifier to reference this object later. Must be a 32-bit integer. + */ + id?: number + /** + * Identifier of the {@link Channel} that deliveres this notification. + * + * If the channel does not exist, the notification won't fire. + * Make sure the channel exists with {@link listChannels} and {@link createChannel}. + */ + channelId?: string + /** + * The title displayed on the notification. + */ + title: string + /** + * Optional notification body. + * */ + body?: string + /** + * Schedule this notification to fire on a later time or a fixed interval. + */ + schedule?: Schedule + /** + * Multiline text. + * Changes the notification style to big text. + * Cannot be used with `inboxLines`. + */ + largeBody?: string + /** + * Detail text for the notification with `largeBody`, `inboxLines` or `groupSummary`. + */ + summary?: string + /** + * Defines an action type for this notification. + */ + actionTypeId?: string + /** + * Identifier used to group multiple notifications. + * + * https://developer.apple.com/documentation/usernotifications/unmutablenotificationcontent/1649872-threadidentifier + */ + group?: string + /** + * Instructs the system that this notification is the summary of a group on Android. + */ + groupSummary?: boolean + /** + * The sound resource name or file path for the notification. + * + * ## Platform-specific behavior: + * + * - On macOS: use system sounds (e.g., "Ping", "Blow") or sound files in the app bundle + * - On Linux: use XDG theme sounds (e.g., "message-new-instant") or file paths + * - On Windows: use file paths to sound files (.wav format) + * - On Mobile: use resource names + */ + sound?: string + /** + * List of lines to add to the notification. + * Changes the notification style to inbox. + * Cannot be used with `largeBody`. + * + * Only supports up to 5 lines. + */ + inboxLines?: string[] + /** + * Notification icon. + * + * On Android the icon must be placed in the app's `res/drawable` folder. + */ + icon?: string + /** + * Notification large icon (Android). + * + * The icon must be placed in the app's `res/drawable` folder. + */ + largeIcon?: string + /** + * Icon color on Android. + */ + iconColor?: string + /** + * Notification attachments. + */ + attachments?: Attachment[] + /** + * Extra payload to store in the notification. + */ + extra?: Record + /** + * If true, the notification cannot be dismissed by the user on Android. + * + * An application service must manage the dismissal of the notification. + * It is typically used to indicate a background task that is pending (e.g. a file download) + * or the user is engaged with (e.g. playing music). + */ + ongoing?: boolean + /** + * Automatically cancel the notification when the user clicks on it. + */ + autoCancel?: boolean + /** + * Changes the notification presentation to be silent on iOS (no badge, no sound, not listed). + */ + silent?: boolean + /** + * Notification visibility. + */ + visibility?: Visibility + /** + * Sets the number of items this notification represents on Android. + */ + number?: number +} + +/** + * The set of date fields a scheduled notification must match to be delivered. + * + * Fields that are not set match any value, so the notification fires on every date + * whose remaining components match. + */ +interface ScheduleInterval { + /** + * The year the notification fires on. + */ + year?: number + /** + * The month of the year the notification fires on. + */ + month?: number + /** + * The day of the month the notification fires on. + */ + day?: number + /** + * The day of the week the notification fires on. + * + * 1 - Sunday + * 2 - Monday + * 3 - Tuesday + * 4 - Wednesday + * 5 - Thursday + * 6 - Friday + * 7 - Saturday + */ + weekday?: number + /** + * The hour of the day the notification fires on, in the 24-hour clock. + */ + hour?: number + /** + * The minute of the hour the notification fires on. + */ + minute?: number + /** + * The second of the minute the notification fires on. + */ + second?: number +} + +/** + * The unit of the repeating interval used by {@link Schedule.every}. + */ +enum ScheduleEvery { + /** + * The notification repeats every year. + * + * On Android a year is approximated as 52 weeks. + */ + Year = 'year', + /** + * The notification repeats every month. + * + * On Android a month is approximated as 30 days. + */ + Month = 'month', + /** + * The notification repeats every two weeks. + */ + TwoWeeks = 'twoWeeks', + /** + * The notification repeats every week. + */ + Week = 'week', + /** + * The notification repeats every day. + */ + Day = 'day', + /** + * The notification repeats every hour. + */ + Hour = 'hour', + /** + * The notification repeats every minute. + */ + Minute = 'minute', + /** + * The notification repeats every second. + * + * Not supported on iOS, where repeating triggers must be at least a minute apart. + */ + Second = 'second' +} + +/** + * Defines when a scheduled notification is delivered. + * + * Build one with the static {@link Schedule.at}, {@link Schedule.interval} and + * {@link Schedule.every} helpers, then pass it to the `schedule` option of a notification. + * Scheduling is only supported on mobile; desktop notifications are always shown immediately. + * + * @since 2.0.0 + */ +class Schedule { + /** + * Set when the notification fires at a fixed date and time. + */ + at: + | { + date: Date + repeating: boolean + allowWhileIdle: boolean + } + | undefined + + /** + * Set when the notification fires whenever the current date matches the given fields. + */ + interval: + | { + interval: ScheduleInterval + allowWhileIdle: boolean + } + | undefined + + /** + * Set when the notification repeats on a fixed interval. + */ + every: + | { + interval: ScheduleEvery + count: number + allowWhileIdle: boolean + } + | undefined + + /** + * Creates a schedule that fires the notification at the given date and time. + * + * @example + * ```typescript + * import { Schedule, sendNotification } from '@tauri-apps/plugin-notification'; + * const schedule = Schedule.at(new Date(Date.now() + 60 * 1000)); + * sendNotification({ title: 'Tauri', body: 'One minute later', schedule }); + * ``` + * + * @param date The date and time the notification fires at. It must be in the future. + * @param repeating Whether the notification keeps repeating, using the duration between the moment it is scheduled and `date` as the interval. The interval must be at least one minute on iOS. + * @param allowWhileIdle Whether the notification is allowed to fire while the device is in low-power idle (Doze) mode. Android only. + * + * @returns A schedule that can be assigned to the `schedule` option of a notification. + */ + static at(date: Date, repeating = false, allowWhileIdle = false): Schedule { + return { + at: { date, repeating, allowWhileIdle }, + interval: undefined, + every: undefined + } + } + + /** + * Creates a schedule that fires the notification whenever the current date matches + * every field set on the given interval. + * + * @example + * ```typescript + * import { Schedule, sendNotification } from '@tauri-apps/plugin-notification'; + * // fires every day at 9:00 + * const schedule = Schedule.interval({ hour: 9, minute: 0 }); + * sendNotification({ title: 'Tauri', body: 'Good morning', schedule }); + * ``` + * + * @param interval The date fields the current date must match for the notification to fire. + * @param allowWhileIdle Whether the notification is allowed to fire while the device is in low-power idle (Doze) mode. Android only. + * + * @returns A schedule that can be assigned to the `schedule` option of a notification. + */ + static interval( + interval: ScheduleInterval, + allowWhileIdle = false + ): Schedule { + return { + at: undefined, + interval: { interval, allowWhileIdle }, + every: undefined + } + } + + /** + * Creates a schedule that repeatedly fires the notification, once every `count` interval units. + * + * @example + * ```typescript + * import { Schedule, ScheduleEvery, sendNotification } from '@tauri-apps/plugin-notification'; + * const schedule = Schedule.every(ScheduleEvery.Hour, 2); + * sendNotification({ title: 'Tauri', body: 'Every two hours', schedule }); + * ``` + * + * @param kind The unit of the repeating interval. + * @param count How many interval units elapse between each notification. + * @param allowWhileIdle Whether the notification is allowed to fire while the device is in low-power idle (Doze) mode. Android only. + * + * @returns A schedule that can be assigned to the `schedule` option of a notification. + */ + static every( + kind: ScheduleEvery, + count: number, + allowWhileIdle = false + ): Schedule { + return { + at: undefined, + interval: undefined, + every: { interval: kind, count, allowWhileIdle } + } + } +} + +/** + * Attachment of a notification. + */ +interface Attachment { + /** Attachment identifier. */ + id: string + /** Attachment URL. Accepts the `asset` and `file` protocols. */ + url: string +} + +/** + * A button the user can tap on a notification, belonging to an {@link ActionType}. + * + * Only used on mobile. On Android only the identifier, the title and the input flag are used. + */ +interface Action { + /** + * The identifier of this action, reported back when the user triggers it. + */ + id: string + /** + * The text displayed on the action button. + */ + title: string + /** + * Whether the device must be unlocked for the action to run. iOS only. + */ + requiresAuthentication?: boolean + /** + * Whether the app is brought to the foreground when the action is triggered. iOS only. + */ + foreground?: boolean + /** + * Whether the action is displayed as destructive, usually in red. iOS only. + */ + destructive?: boolean + /** + * Whether triggering the action lets the user type a text response. + */ + input?: boolean + /** + * The text displayed on the button that submits the text input. iOS only. + */ + inputButtonTitle?: string + /** + * The placeholder displayed on the empty text input field. iOS only. + */ + inputPlaceholder?: string +} + +/** + * A group of {@link Action}s a notification can display, referenced by the + * `actionTypeId` option of a notification. + * + * Register it with {@link registerActionTypes} before sending a notification that uses it. + * Only used on mobile. + */ +interface ActionType { + /** + * The identifier of this action type + */ + id: string + /** + * The list of associated actions + */ + actions: Action[] + /** + * The placeholder shown instead of the notification body when previews are hidden. iOS only. + */ + hiddenPreviewsBodyPlaceholder?: string + /** + * Whether the app is notified when the user dismisses the notification. iOS only. + */ + customDismissAction?: boolean + /** + * Whether the notification can be displayed in a CarPlay environment. iOS only. + */ + allowInCarPlay?: boolean + /** + * Whether the notification title is shown even when previews are hidden. iOS only. + */ + hiddenPreviewsShowTitle?: boolean + /** + * Whether the notification subtitle is shown even when previews are hidden. iOS only. + */ + hiddenPreviewsShowSubtitle?: boolean +} + +/** + * A notification that was scheduled and has not been delivered yet. + * + * Returned by {@link pending}, which is only supported on mobile. + */ +interface PendingNotification { + /** + * The identifier of the notification. + */ + id: number + /** + * The title of the notification, if it was set. + */ + title?: string + /** + * The body of the notification, if it was set. + */ + body?: string + /** + * The schedule that determines when the notification is delivered. + */ + schedule: Schedule +} + +/** + * A notification that was delivered and is still visible in the notification center. + * + * Returned by {@link active}, which is only supported on mobile. Which fields are + * populated depends on the platform, since Android and iOS expose different + * information about delivered notifications. + */ +interface ActiveNotification { + /** + * The identifier of the notification. + */ + id: number + /** + * The tag the notification was posted with. Android only. + */ + tag?: string + /** + * The title of the notification, if it was set. + */ + title?: string + /** + * The body of the notification, if it was set. + */ + body?: string + /** + * The identifier of the group the notification belongs to. Android only. + */ + group?: string + /** + * Whether the notification is the summary of its group. Android only. + */ + groupSummary: boolean + /** + * The platform extras attached to the notification, as string values. Android only. + */ + data: Record + /** + * The extra payload that was stored in the notification. + */ + extra: Record + /** + * The attachments of the notification. iOS only. + */ + attachments: Attachment[] + /** + * The identifier of the action type the notification was registered with. iOS only. + */ + actionTypeId?: string + /** + * The schedule the notification was delivered with, if it was scheduled. + */ + schedule?: Schedule + /** + * The sound resource name of the notification. iOS only. + */ + sound?: string +} + +/** + * How much the notifications of a {@link Channel} interrupt the user. + * + * It maps to the Android `NotificationManager.IMPORTANCE_*` constants and is only used on Android. + */ +enum Importance { + /** + * The notifications are not shown. + */ + None = 0, + /** + * The notifications are only shown in the shade, below the fold, without a status bar icon. + */ + Min, + /** + * The notifications are shown without a sound. + */ + Low, + /** + * The notifications are shown and make a sound. + * + * This is the value used when the channel does not define an importance. + */ + Default, + /** + * The notifications are shown, make a sound and pop up as a heads-up notification. + */ + High +} + +/** + * How much of a notification is shown on the lock screen. + * + * It maps to the Android `Notification.VISIBILITY_*` constants and is only used on Android. + */ +enum Visibility { + /** + * The notification is not shown on the lock screen at all. + */ + Secret = -1, + /** + * The notification is shown on the lock screen with its sensitive content hidden. + * + * This is the value used when the channel does not define a visibility. + */ + Private, + /** + * The notification is shown in full on the lock screen. + */ + Public +} + +/** + * A notification channel, the category users configure notification behavior on. + * + * Notifications reference a channel through their `channelId` option and are not delivered + * when the channel does not exist. Channels are only supported on Android. + */ +interface Channel { + /** + * The identifier of this channel. + */ + id: string + /** + * The user visible name of this channel. + */ + name: string + /** + * The user visible description of this channel. + */ + description?: string + /** + * The name of the sound resource played by the notifications of this channel. + * + * The resource must be placed in the app's `res/raw` folder. + */ + sound?: string + /** + * Whether the notifications of this channel blink the device light. + */ + lights?: boolean + /** + * The color of the device light, as a color string such as `#ff0000`. + */ + lightColor?: string + /** + * Whether the notifications of this channel vibrate the device. + */ + vibration?: boolean + /** + * How much the notifications of this channel interrupt the user. + */ + importance?: Importance + /** + * How much of the notifications of this channel is shown on the lock screen. + */ + visibility?: Visibility +} + +/** + * Checks if the permission to send notifications is granted. + * @example + * ```typescript + * import { isPermissionGranted } from '@tauri-apps/plugin-notification'; + * const permissionGranted = await isPermissionGranted(); + * ``` + * + * @returns A promise resolving to whether the permission to send notifications is granted. + * + * @since 2.0.0 + */ +async function isPermissionGranted(): Promise { + if (window.Notification.permission !== 'default') { + return await Promise.resolve(window.Notification.permission === 'granted') + } + return await invoke('plugin:notification|is_permission_granted') +} + +/** + * Requests the permission to send notifications. + * @example + * ```typescript + * import { isPermissionGranted, requestPermission } from '@tauri-apps/plugin-notification'; + * let permissionGranted = await isPermissionGranted(); + * if (!permissionGranted) { + * const permission = await requestPermission(); + * permissionGranted = permission === 'granted'; + * } + * ``` + * + * @returns A promise resolving to whether the user granted the permission or not. + * + * @since 2.0.0 + */ +async function requestPermission(): Promise { + return await window.Notification.requestPermission() +} + +/** + * Sends a notification to the user. + * @example + * ```typescript + * import { isPermissionGranted, requestPermission, sendNotification } from '@tauri-apps/plugin-notification'; + * let permissionGranted = await isPermissionGranted(); + * if (!permissionGranted) { + * const permission = await requestPermission(); + * permissionGranted = permission === 'granted'; + * } + * if (permissionGranted) { + * sendNotification('Tauri is awesome!'); + * sendNotification({ title: 'TAURI', body: 'Tauri is awesome!' }); + * } + * ``` + * + * @param options The notification content, or the notification title when a string is given. + * + * @since 2.0.0 + */ +function sendNotification(options: Options | string): void { + if (typeof options === 'string') { + new window.Notification(options) + } else { + new window.Notification(options.title, options) + } +} + +/** + * Register actions that are performed when the user clicks on the notification. + * + * @example + * ```typescript + * import { registerActionTypes } from '@tauri-apps/plugin-notification'; + * await registerActionTypes([{ + * id: 'tauri', + * actions: [{ + * id: 'my-action', + * title: 'Settings' + * }] + * }]) + * ``` + * + * @param types The action types to register. + * + * @returns A promise indicating the success or failure of the operation. + * + * @since 2.0.0 + */ +async function registerActionTypes(types: ActionType[]): Promise { + await invoke('plugin:notification|register_action_types', { types }) +} + +/** + * Retrieves the list of pending notifications. + * + * @example + * ```typescript + * import { pending } from '@tauri-apps/plugin-notification'; + * const pendingNotifications = await pending(); + * ``` + * + * @returns A promise resolving to the list of pending notifications. + * + * @since 2.0.0 + */ +async function pending(): Promise { + return await invoke('plugin:notification|get_pending') +} + +/** + * Cancels the pending notifications with the given list of identifiers. + * + * @example + * ```typescript + * import { cancel } from '@tauri-apps/plugin-notification'; + * await cancel([-34234, 23432, 4311]); + * ``` + * + * @param notifications The identifiers of the pending notifications to cancel. + * + * @returns A promise indicating the success or failure of the operation. + * + * @since 2.0.0 + */ +async function cancel(notifications: number[]): Promise { + await invoke('plugin:notification|cancel', { notifications }) +} + +/** + * Cancels all pending notifications. + * + * @example + * ```typescript + * import { cancelAll } from '@tauri-apps/plugin-notification'; + * await cancelAll(); + * ``` + * + * @returns A promise indicating the success or failure of the operation. + * + * @since 2.0.0 + */ +async function cancelAll(): Promise { + await invoke('plugin:notification|cancel') +} + +/** + * Retrieves the list of active notifications. + * + * @example + * ```typescript + * import { active } from '@tauri-apps/plugin-notification'; + * const activeNotifications = await active(); + * ``` + * + * @returns A promise resolving to the list of active notifications. + * + * @since 2.0.0 + */ +async function active(): Promise { + return await invoke('plugin:notification|get_active') +} + +/** + * Removes the active notifications with the given list of identifiers. + * + * @example + * ```typescript + * import { removeActive } from '@tauri-apps/plugin-notification'; + * await removeActive([{ id: -34234 }, { id: 23432 }, { id: 4311 }]) + * ``` + * + * @param notifications The active notifications to remove, identified by their id and, on Android, their optional tag. + * + * @returns A promise indicating the success or failure of the operation. + * + * @since 2.0.0 + */ +async function removeActive( + notifications: Array<{ id: number; tag?: string }> +): Promise { + await invoke('plugin:notification|remove_active', { notifications }) +} + +/** + * Removes all active notifications. + * + * @example + * ```typescript + * import { removeAllActive } from '@tauri-apps/plugin-notification'; + * await removeAllActive() + * ``` + * + * @returns A promise indicating the success or failure of the operation. + * + * @since 2.0.0 + */ +async function removeAllActive(): Promise { + await invoke('plugin:notification|remove_active') +} + +/** + * Creates a notification channel. + * + * @example + * ```typescript + * import { createChannel, Importance, Visibility } from '@tauri-apps/plugin-notification'; + * await createChannel({ + * id: 'new-messages', + * name: 'New Messages', + * lights: true, + * vibration: true, + * importance: Importance.Default, + * visibility: Visibility.Private + * }); + * ``` + * + * @param channel The channel to create. + * + * @returns A promise indicating the success or failure of the operation. + * + * @since 2.0.0 + */ +async function createChannel(channel: Channel): Promise { + await invoke('plugin:notification|create_channel', { ...channel }) +} + +/** + * Removes the channel with the given identifier. + * + * @example + * ```typescript + * import { removeChannel } from '@tauri-apps/plugin-notification'; + * await removeChannel('new-messages'); + * ``` + * + * @param id The identifier of the channel to remove. + * + * @returns A promise indicating the success or failure of the operation. + * + * @since 2.0.0 + */ +async function removeChannel(id: string): Promise { + await invoke('plugin:notification|delete_channel', { id }) +} + +/** + * Retrieves the list of notification channels. + * + * @example + * ```typescript + * import { channels } from '@tauri-apps/plugin-notification'; + * const notificationChannels = await channels(); + * ``` + * + * @returns A promise resolving to the list of notification channels. + * + * @since 2.0.0 + */ +async function channels(): Promise { + return await invoke('plugin:notification|list_channels') +} + +/** + * Listens to notifications that are delivered while the app is running. + * + * Only emitted on mobile. + * + * @example + * ```typescript + * import { onNotificationReceived } from '@tauri-apps/plugin-notification'; + * const unlisten = await onNotificationReceived((notification) => { + * console.log(`received notification: ${notification.title}`); + * }); + * ``` + * + * @param cb The closure called with the notification that was delivered. + * + * @returns A promise resolving to a listener that can be used to stop listening for the event. + * + * @since 2.0.0 + */ +async function onNotificationReceived( + cb: (notification: Options) => void +): Promise { + return await addPluginListener('notification', 'notification', cb) +} + +/** + * Listens to the actions the user performs on a notification. + * + * Only emitted on mobile, for notifications that reference an action type + * registered with {@link registerActionTypes}. + * + * @example + * ```typescript + * import { onAction } from '@tauri-apps/plugin-notification'; + * const unlisten = await onAction((notification) => { + * console.log(`user acted on notification: ${notification.title}`); + * }); + * ``` + * + * @param cb The closure called with the notification the action was performed on. + * + * @returns A promise resolving to a listener that can be used to stop listening for the event. + * + * @since 2.0.0 + */ +async function onAction( + cb: (notification: Options) => void +): Promise { + return await addPluginListener('notification', 'actionPerformed', cb) +} + +export type { + Attachment, + Options, + Action, + ActionType, + PendingNotification, + ActiveNotification, + Channel, + ScheduleInterval +} + +export { + Importance, + Visibility, + sendNotification, + requestPermission, + isPermissionGranted, + registerActionTypes, + pending, + cancel, + cancelAll, + active, + removeActive, + removeAllActive, + createChannel, + removeChannel, + channels, + onNotificationReceived, + onAction, + Schedule, + ScheduleEvery +} diff --git a/plugins/notification/guest-js/init.ts b/plugins/notification/guest-js/init.ts new file mode 100644 index 0000000..3d1780a --- /dev/null +++ b/plugins/notification/guest-js/init.ts @@ -0,0 +1,107 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +/** + * Webview script injected by the notification plugin. + * + * It replaces `window.Notification` with an implementation backed by the plugin, so the + * standard Notification Web API sends OS notifications, and keeps + * `window.Notification.permission` and `window.Notification.requestPermission` in sync + * with the permission state reported by the plugin. + * + * @module + */ + +import { invoke } from '@tauri-apps/api/core' +import type { PermissionState } from '@tauri-apps/api/core' +import type { Options } from './index' +;(function () { + let permissionSettable = false + let permissionValue = 'default' + + async function isPermissionGranted(): Promise { + if (window.Notification.permission !== 'default') { + return await Promise.resolve(window.Notification.permission === 'granted') + } + // Windows always grants the permission, and asking the backend for it from + // the init script makes the WebView throw a STATUS_ACCESS_VIOLATION on + // remote websites, so answer it here instead of invoking. + // @ts-expect-error __TEMPLATE_windows__ will be replaced in rust before it's injected. + if (__TEMPLATE_windows__) { + return true + } + return await invoke('plugin:notification|is_permission_granted') + } + + function setNotificationPermission(value: NotificationPermission): void { + permissionSettable = true + // @ts-expect-error we can actually set this value on the webview + window.Notification.permission = value + permissionSettable = false + } + + async function requestPermission(): Promise { + return await invoke( + 'plugin:notification|request_permission' + ).then((permission) => { + setNotificationPermission( + permission === 'prompt' || permission === 'prompt-with-rationale' + ? 'default' + : permission + ) + return permission + }) + } + + async function sendNotification(options: string | Options): Promise { + if (typeof options === 'object') { + Object.freeze(options) + } + + await invoke('plugin:notification|notify', { + options: + typeof options === 'string' + ? { + title: options + } + : options + }) + } + + // @ts-expect-error unfortunately we can't implement the whole type, so we overwrite it with our own version + window.Notification = function (title, options) { + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + const opts = options || {} + void sendNotification( + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument + Object.assign(opts, { + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + title + }) + ) + } + + // @ts-expect-error tauri does not have sync IPC :( + window.Notification.requestPermission = requestPermission + + Object.defineProperty(window.Notification, 'permission', { + enumerable: true, + get: () => permissionValue, + set: (v) => { + if (!permissionSettable) { + throw new Error('Readonly property') + } + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + permissionValue = v + } + }) + + void isPermissionGranted().then(function (response) { + if (response === null) { + setNotificationPermission('default') + } else { + setNotificationPermission(response ? 'granted' : 'denied') + } + }) +})() diff --git a/plugins/notification/ios/.gitignore b/plugins/notification/ios/.gitignore new file mode 100644 index 0000000..5922fda --- /dev/null +++ b/plugins/notification/ios/.gitignore @@ -0,0 +1,10 @@ +.DS_Store +/.build +/Packages +/*.xcodeproj +xcuserdata/ +DerivedData/ +.swiftpm/config/registries.json +.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata +.netrc +Package.resolved diff --git a/plugins/notification/ios/Package.swift b/plugins/notification/ios/Package.swift new file mode 100644 index 0000000..bbd6df9 --- /dev/null +++ b/plugins/notification/ios/Package.swift @@ -0,0 +1,34 @@ +// swift-tools-version:5.5 +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +import PackageDescription + +let package = Package( + name: "tauri-plugin-notification", + platforms: [ + .macOS(.v10_13), + .iOS(.v13), + ], + products: [ + // Products define the executables and libraries a package produces, and make them visible to other packages. + .library( + name: "tauri-plugin-notification", + type: .static, + targets: ["tauri-plugin-notification"]) + ], + dependencies: [ + .package(name: "Tauri", path: "../.tauri/tauri-api") + ], + targets: [ + // Targets are the basic building blocks of a package. A target can define a module or a test suite. + // Targets can depend on other targets in this package, and on products in packages this package depends on. + .target( + name: "tauri-plugin-notification", + dependencies: [ + .byName(name: "Tauri") + ], + path: "Sources") + ] +) diff --git a/plugins/notification/ios/README.md b/plugins/notification/ios/README.md new file mode 100644 index 0000000..f4900bd --- /dev/null +++ b/plugins/notification/ios/README.md @@ -0,0 +1,3 @@ +# Tauri Plugin {{ plugin_name_original }} + +A description of this package. diff --git a/plugins/notification/ios/Sources/Notification.swift b/plugins/notification/ios/Sources/Notification.swift new file mode 100644 index 0000000..259399b --- /dev/null +++ b/plugins/notification/ios/Sources/Notification.swift @@ -0,0 +1,286 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +import Tauri +import UserNotifications + +enum NotificationError: LocalizedError { + case triggerRepeatIntervalTooShort + case attachmentFileNotFound(path: String) + case attachmentUnableToCreate(String) + case pastScheduledTime + case invalidDate(String) + + var errorDescription: String? { + switch self { + case .triggerRepeatIntervalTooShort: + return "Schedule interval too short, must be a least 1 minute" + case .attachmentFileNotFound(let path): + return "Unable to find file \(path) for attachment" + case .attachmentUnableToCreate(let error): + return "Failed to create attachment: \(error)" + case .pastScheduledTime: + return "Scheduled time must be *after* current time" + case .invalidDate(let date): + return "Could not parse date \(date)" + } + } +} + +func makeNotificationContent(_ notification: Notification) throws -> UNNotificationContent { + let content = UNMutableNotificationContent() + content.title = NSString.localizedUserNotificationString( + forKey: notification.title, arguments: nil) + if let body = notification.body { + content.body = NSString.localizedUserNotificationString( + forKey: body, + arguments: nil) + } + + var userInfo: [String: Any] = [:] + + if let extra = notification.extra { + userInfo["__EXTRA__"] = extra + } + + if let schedule = notification.schedule { + userInfo["__SCHEDULE__"] = scheduleToDictionary(schedule) + } + + content.userInfo = userInfo + + if let actionTypeId = notification.actionTypeId { + content.categoryIdentifier = actionTypeId + } + + if let threadIdentifier = notification.group { + content.threadIdentifier = threadIdentifier + } + + if let summaryArgument = notification.summary { + content.summaryArgument = summaryArgument + } + + if let sound = notification.sound { + content.sound = UNNotificationSound(named: UNNotificationSoundName(sound)) + } + + if let attachments = notification.attachments { + content.attachments = try makeAttachments(attachments) + } + + return content +} + +func scheduleToDictionary(_ schedule: NotificationSchedule) -> [String: Any] { + switch schedule { + case .at(let date, let repeating): + return [ + "type": "at", + "date": date, + "repeating": repeating + ] + case .interval(let interval): + return [ + "type": "interval", + "interval": scheduleIntervalToDictionary(interval) + ] + case .every(let interval, let count): + return [ + "type": "every", + "interval": interval.rawValue, + "count": count + ] + } +} + +func scheduleIntervalToDictionary(_ interval: ScheduleInterval) -> [String: Any] { + var dict: [String: Any] = [:] + + if let year = interval.year { + dict["year"] = year + } + if let month = interval.month { + dict["month"] = month + } + if let day = interval.day { + dict["day"] = day + } + if let weekday = interval.weekday { + dict["weekday"] = weekday + } + if let hour = interval.hour { + dict["hour"] = hour + } + if let minute = interval.minute { + dict["minute"] = minute + } + if let second = interval.second { + dict["second"] = second + } + + return dict +} + +func makeAttachments(_ attachments: [NotificationAttachment]) throws -> [UNNotificationAttachment] { + var createdAttachments = [UNNotificationAttachment]() + + for attachment in attachments { + + guard let urlObject = makeAttachmentUrl(attachment.url) else { + throw NotificationError.attachmentFileNotFound(path: attachment.url) + } + + let options = attachment.options != nil ? makeAttachmentOptions(attachment.options!) : nil + + do { + let newAttachment = try UNNotificationAttachment( + identifier: attachment.id, url: urlObject, options: options) + createdAttachments.append(newAttachment) + } catch { + throw NotificationError.attachmentUnableToCreate(error.localizedDescription) + } + } + + return createdAttachments +} + +func makeAttachmentUrl(_ path: String) -> URL? { + return URL(string: path) +} + +func makeAttachmentOptions(_ options: NotificationAttachmentOptions) -> [AnyHashable: Any] { + var opts: [AnyHashable: Any] = [:] + + if let value = options.iosUNNotificationAttachmentOptionsTypeHintKey { + opts[UNNotificationAttachmentOptionsTypeHintKey] = value + } + if let value = options.iosUNNotificationAttachmentOptionsThumbnailHiddenKey { + opts[UNNotificationAttachmentOptionsThumbnailHiddenKey] = value + } + if let value = options.iosUNNotificationAttachmentOptionsThumbnailClippingRectKey { + opts[UNNotificationAttachmentOptionsThumbnailClippingRectKey] = value + } + if let value = options + .iosUNNotificationAttachmentOptionsThumbnailTimeKey + + { + opts[UNNotificationAttachmentOptionsThumbnailTimeKey] = value + } + return opts +} + +func handleScheduledNotification(_ schedule: NotificationSchedule) throws + -> UNNotificationTrigger? +{ + switch schedule { + case .at(let date, let repeating): + let dateFormatter = DateFormatter() + dateFormatter.locale = Locale(identifier: "en_US_POSIX") + dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'" + + if let at = dateFormatter.date(from: date) { + let dateInfo = Calendar.current.dateComponents(in: TimeZone.current, from: at) + + if dateInfo.date! < Date() { + throw NotificationError.pastScheduledTime + } + + let dateInterval = DateInterval(start: Date(), end: dateInfo.date!) + + // Notifications that repeat have to be at least a minute between each other + if repeating && dateInterval.duration < 60 { + throw NotificationError.triggerRepeatIntervalTooShort + } + + return UNTimeIntervalNotificationTrigger( + timeInterval: dateInterval.duration, repeats: repeating) + + } else { + throw NotificationError.invalidDate(date) + } + case .interval(let interval): + let dateComponents = getDateComponents(interval) + return UNCalendarNotificationTrigger(dateMatching: dateComponents, repeats: true) + case .every(let interval, let count): + if let repeatDateInterval = getRepeatDateInterval(interval, count) { + // Notifications that repeat have to be at least a minute between each other + if repeatDateInterval.duration < 60 { + throw NotificationError.triggerRepeatIntervalTooShort + } + + return UNTimeIntervalNotificationTrigger( + timeInterval: repeatDateInterval.duration, repeats: true) + } + } + + return nil +} + +/// Given our schedule format, return a DateComponents object +/// that only contains the components passed in. + +func getDateComponents(_ at: ScheduleInterval) -> DateComponents { + // var dateInfo = Calendar.current.dateComponents(in: TimeZone.current, from: Date()) + // dateInfo.calendar = Calendar.current + var dateInfo = DateComponents() + + if let year = at.year { + dateInfo.year = year + } + if let month = at.month { + dateInfo.month = month + } + if let day = at.day { + dateInfo.day = day + } + if let hour = at.hour { + dateInfo.hour = hour + } + if let minute = at.minute { + dateInfo.minute = minute + } + if let second = at.second { + dateInfo.second = second + } + if let weekday = at.weekday { + dateInfo.weekday = weekday + } + return dateInfo +} + +/// Compute the difference between the string representation of a date +/// interval and today. For example, if every is "month", then we +/// return the interval between today and a month from today. + +func getRepeatDateInterval(_ every: ScheduleEveryKind, _ count: Int) -> DateInterval? { + let cal = Calendar.current + let now = Date() + switch every { + case .year: + let newDate = cal.date(byAdding: .year, value: count, to: now)! + return DateInterval(start: now, end: newDate) + case .month: + let newDate = cal.date(byAdding: .month, value: count, to: now)! + return DateInterval(start: now, end: newDate) + case .twoWeeks: + let newDate = cal.date(byAdding: .weekOfYear, value: 2 * count, to: now)! + return DateInterval(start: now, end: newDate) + case .week: + let newDate = cal.date(byAdding: .weekOfYear, value: count, to: now)! + return DateInterval(start: now, end: newDate) + case .day: + let newDate = cal.date(byAdding: .day, value: count, to: now)! + return DateInterval(start: now, end: newDate) + case .hour: + let newDate = cal.date(byAdding: .hour, value: count, to: now)! + return DateInterval(start: now, end: newDate) + case .minute: + let newDate = cal.date(byAdding: .minute, value: count, to: now)! + return DateInterval(start: now, end: newDate) + case .second: + let newDate = cal.date(byAdding: .second, value: count, to: now)! + return DateInterval(start: now, end: newDate) + } +} diff --git a/plugins/notification/ios/Sources/NotificationCategory.swift b/plugins/notification/ios/Sources/NotificationCategory.swift new file mode 100644 index 0000000..f796e03 --- /dev/null +++ b/plugins/notification/ios/Sources/NotificationCategory.swift @@ -0,0 +1,97 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +import Tauri +import UserNotifications + +internal func makeCategories(_ actionTypes: [ActionType]) { + var createdCategories = [UNNotificationCategory]() + + let generalCategory = UNNotificationCategory( + identifier: "GENERAL", + actions: [], + intentIdentifiers: [], + options: .customDismissAction) + + createdCategories.append(generalCategory) + for type in actionTypes { + let newActions = makeActions(type.actions) + + // Create the custom actions for the TIMER_EXPIRED category. + var newCategory: UNNotificationCategory? + + newCategory = UNNotificationCategory( + identifier: type.id, + actions: newActions, + intentIdentifiers: [], + hiddenPreviewsBodyPlaceholder: type.hiddenBodyPlaceholder ?? "", + options: makeCategoryOptions(type)) + + createdCategories.append(newCategory!) + } + + let center = UNUserNotificationCenter.current() + center.setNotificationCategories(Set(createdCategories)) +} + +func makeActions(_ actions: [Action]) -> [UNNotificationAction] { + var createdActions = [UNNotificationAction]() + + for action in actions { + var newAction: UNNotificationAction + if action.input ?? false { + if action.inputButtonTitle != nil { + newAction = UNTextInputNotificationAction( + identifier: action.id, + title: action.title, + options: makeActionOptions(action), + textInputButtonTitle: action.inputButtonTitle ?? "", + textInputPlaceholder: action.inputPlaceholder ?? "") + } else { + newAction = UNTextInputNotificationAction( + identifier: action.id, title: action.title, options: makeActionOptions(action)) + } + } else { + // Create the custom actions for the TIMER_EXPIRED category. + newAction = UNNotificationAction( + identifier: action.id, + title: action.title, + options: makeActionOptions(action)) + } + createdActions.append(newAction) + } + + return createdActions +} + +func makeActionOptions(_ action: Action) -> UNNotificationActionOptions { + if action.foreground ?? false { + return .foreground + } + if action.destructive ?? false { + return .destructive + } + if action.requiresAuthentication ?? false { + return .authenticationRequired + } + return UNNotificationActionOptions(rawValue: 0) +} + +func makeCategoryOptions(_ type: ActionType) -> UNNotificationCategoryOptions { + if type.customDismissAction ?? false { + return .customDismissAction + } + if type.allowInCarPlay ?? false { + return .allowInCarPlay + } + + if type.hiddenPreviewsShowTitle ?? false { + return .hiddenPreviewsShowTitle + } + if type.hiddenPreviewsShowSubtitle ?? false { + return .hiddenPreviewsShowSubtitle + } + + return UNNotificationCategoryOptions(rawValue: 0) +} diff --git a/plugins/notification/ios/Sources/NotificationHandler.swift b/plugins/notification/ios/Sources/NotificationHandler.swift new file mode 100644 index 0000000..1bf134b --- /dev/null +++ b/plugins/notification/ios/Sources/NotificationHandler.swift @@ -0,0 +1,118 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +import Tauri +import UserNotifications + +public class NotificationHandler: NSObject, NotificationHandlerProtocol { + + public weak var plugin: Plugin? + + private var notificationsMap = [String: Notification]() + + internal func saveNotification(_ key: String, _ notification: Notification) { + notificationsMap.updateValue(notification, forKey: key) + } + + public func requestPermissions(with completion: ((Bool, Error?) -> Void)? = nil) { + let center = UNUserNotificationCenter.current() + center.requestAuthorization(options: [.badge, .alert, .sound]) { (granted, error) in + completion?(granted, error) + } + } + + public func checkPermissions(with completion: ((UNAuthorizationStatus) -> Void)? = nil) { + let center = UNUserNotificationCenter.current() + center.getNotificationSettings { settings in + completion?(settings.authorizationStatus) + } + } + + public func willPresent(notification: UNNotification) -> UNNotificationPresentationOptions { + let notificationData = toActiveNotification(notification.request) + try? self.plugin?.trigger("notification", data: notificationData) + + if let options = notificationsMap[notification.request.identifier] { + if options.silent ?? false { + return UNNotificationPresentationOptions.init(rawValue: 0) + } + } + + return [ + .badge, + .sound, + .alert, + ] + } + + public func didReceive(response: UNNotificationResponse) { + let originalNotificationRequest = response.notification.request + let actionId = response.actionIdentifier + + var actionIdValue: String + // We turn the two default actions (open/dismiss) into generic strings + if actionId == UNNotificationDefaultActionIdentifier { + actionIdValue = "tap" + } else if actionId == UNNotificationDismissActionIdentifier { + actionIdValue = "dismiss" + } else { + actionIdValue = actionId + } + + var inputValue: String? = nil + // If the type of action was for an input type, get the value + if let inputType = response as? UNTextInputNotificationResponse { + inputValue = inputType.userText + } + + try? self.plugin?.trigger( + "actionPerformed", + data: ReceivedNotification( + actionId: actionIdValue, + inputValue: inputValue, + notification: toActiveNotification(originalNotificationRequest) + )) + } + + func toActiveNotification(_ request: UNNotificationRequest) -> ActiveNotification { + let notificationRequest = notificationsMap[request.identifier]! + return ActiveNotification( + id: Int(request.identifier) ?? -1, + title: request.content.title, + body: request.content.body, + sound: notificationRequest.sound ?? "", + actionTypeId: request.content.categoryIdentifier, + attachments: notificationRequest.attachments + ) + } + + func toPendingNotification(_ request: UNNotificationRequest) -> PendingNotification { + return PendingNotification( + id: Int(request.identifier) ?? -1, + title: request.content.title, + body: request.content.body + ) + } +} + +struct PendingNotification: Encodable { + let id: Int + let title: String + let body: String +} + +struct ActiveNotification: Encodable { + let id: Int + let title: String + let body: String + let sound: String + let actionTypeId: String + let attachments: [NotificationAttachment]? +} + +struct ReceivedNotification: Encodable { + let actionId: String + let inputValue: String? + let notification: ActiveNotification +} diff --git a/plugins/notification/ios/Sources/NotificationManager.swift b/plugins/notification/ios/Sources/NotificationManager.swift new file mode 100644 index 0000000..81a585b --- /dev/null +++ b/plugins/notification/ios/Sources/NotificationManager.swift @@ -0,0 +1,47 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +import Foundation +import UserNotifications + +@objc public protocol NotificationHandlerProtocol { + func willPresent(notification: UNNotification) -> UNNotificationPresentationOptions + func didReceive(response: UNNotificationResponse) +} + +@objc public class NotificationManager: NSObject, UNUserNotificationCenterDelegate { + public weak var notificationHandler: NotificationHandlerProtocol? + + override init() { + super.init() + let center = UNUserNotificationCenter.current() + center.delegate = self + } + + public func userNotificationCenter( + _ center: UNUserNotificationCenter, + willPresent notification: UNNotification, + withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void + ) { + var presentationOptions: UNNotificationPresentationOptions? = nil + + if notification.request.trigger?.isKind(of: UNPushNotificationTrigger.self) != true { + presentationOptions = notificationHandler?.willPresent(notification: notification) + } + + completionHandler(presentationOptions ?? []) + } + + public func userNotificationCenter( + _ center: UNUserNotificationCenter, + didReceive response: UNNotificationResponse, + withCompletionHandler completionHandler: @escaping () -> Void + ) { + if response.notification.request.trigger?.isKind(of: UNPushNotificationTrigger.self) != true { + notificationHandler?.didReceive(response: response) + } + + completionHandler() + } +} diff --git a/plugins/notification/ios/Sources/NotificationPlugin.swift b/plugins/notification/ios/Sources/NotificationPlugin.swift new file mode 100644 index 0000000..8adbff9 --- /dev/null +++ b/plugins/notification/ios/Sources/NotificationPlugin.swift @@ -0,0 +1,290 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +import SwiftRs +import Tauri +import UIKit +import UserNotifications +import WebKit + +enum ShowNotificationError: LocalizedError { + case make(Error) + case create(Error) + + var errorDescription: String? { + switch self { + case .make(let error): + return "Unable to make notification: \(error)" + case .create(let error): + return "Unable to create notification: \(error)" + } + } +} + +enum ScheduleEveryKind: String, Decodable { + case year + case month + case twoWeeks + case week + case day + case hour + case minute + case second +} + +struct ScheduleInterval: Decodable { + var year: Int? + var month: Int? + var day: Int? + var weekday: Int? + var hour: Int? + var minute: Int? + var second: Int? +} + +enum NotificationSchedule: Decodable { + case at(date: String, repeating: Bool) + case interval(interval: ScheduleInterval) + case every(interval: ScheduleEveryKind, count: Int) +} + +struct NotificationAttachmentOptions: Codable { + let iosUNNotificationAttachmentOptionsTypeHintKey: String? + let iosUNNotificationAttachmentOptionsThumbnailHiddenKey: String? + let iosUNNotificationAttachmentOptionsThumbnailClippingRectKey: String? + let iosUNNotificationAttachmentOptionsThumbnailTimeKey: String? +} + +struct NotificationAttachment: Codable { + let id: String + let url: String + let options: NotificationAttachmentOptions? +} + +struct Notification: Decodable { + let id: Int + var title: String + var body: String? + var extra: [String: String]? + var schedule: NotificationSchedule? + var attachments: [NotificationAttachment]? + var sound: String? + var group: String? + var actionTypeId: String? + var summary: String? + var silent: Bool? +} + +struct RemoveActiveNotification: Decodable { + let id: Int +} + +struct RemoveActiveArgs: Decodable { + let notifications: [RemoveActiveNotification] +} + +func showNotification(invoke: Invoke, notification: Notification) + throws -> UNNotificationRequest +{ + var content: UNNotificationContent + do { + content = try makeNotificationContent(notification) + } catch { + throw ShowNotificationError.make(error) + } + + var trigger: UNNotificationTrigger? + + do { + if let schedule = notification.schedule { + try trigger = handleScheduledNotification(schedule) + } + } catch { + throw ShowNotificationError.create(error) + } + + // Schedule the request. + let request = UNNotificationRequest( + identifier: "\(notification.id)", content: content, trigger: trigger + ) + + let center = UNUserNotificationCenter.current() + center.add(request) { (error: Error?) in + if let theError = error { + invoke.reject(theError.localizedDescription) + } + } + + return request +} + +struct CancelArgs: Decodable { + // `cancelAll()` sends no list: cancel every pending notification + let notifications: [Int]? +} + +struct Action: Decodable { + let id: String + let title: String + var requiresAuthentication: Bool? + var foreground: Bool? + var destructive: Bool? + var input: Bool? + var inputButtonTitle: String? + var inputPlaceholder: String? +} + +struct ActionType: Decodable { + let id: String + let actions: [Action] + var hiddenPreviewsBodyPlaceholder: String? + var customDismissAction: Bool? + var allowInCarPlay: Bool? + var hiddenPreviewsShowTitle: Bool? + var hiddenPreviewsShowSubtitle: Bool? + var hiddenBodyPlaceholder: String? +} + +struct RegisterActionTypesArgs: Decodable { + let types: [ActionType] +} + +struct BatchArgs: Decodable { + let notifications: [Notification] +} + +class NotificationPlugin: Plugin { + let notificationHandler = NotificationHandler() + let notificationManager = NotificationManager() + + override init() { + super.init() + notificationManager.notificationHandler = notificationHandler + notificationHandler.plugin = self + } + + @objc public func show(_ invoke: Invoke) throws { + let notification = try invoke.parseArgs(Notification.self) + + let request = try showNotification(invoke: invoke, notification: notification) + notificationHandler.saveNotification(request.identifier, notification) + invoke.resolve(Int(request.identifier) ?? -1) + } + + @objc public func batch(_ invoke: Invoke) throws { + let args = try invoke.parseArgs(BatchArgs.self) + var ids = [Int]() + + for notification in args.notifications { + let request = try showNotification(invoke: invoke, notification: notification) + notificationHandler.saveNotification(request.identifier, notification) + ids.append(Int(request.identifier) ?? -1) + } + + invoke.resolve(ids) + } + + @objc public override func requestPermissions(_ invoke: Invoke) { + notificationHandler.requestPermissions { granted, error in + guard error == nil else { + invoke.reject(error!.localizedDescription) + return + } + invoke.resolve(["permissionState": granted ? "granted" : "denied"]) + } + } + + @objc public override func checkPermissions(_ invoke: Invoke) { + notificationHandler.checkPermissions { status in + let permission: String + + switch status { + case .authorized, .ephemeral, .provisional: + permission = "granted" + case .denied: + permission = "denied" + case .notDetermined: + permission = "prompt" + @unknown default: + permission = "prompt" + } + + invoke.resolve(["permissionState": permission]) + } + } + + @objc func cancel(_ invoke: Invoke) throws { + let args = try invoke.parseArgs(CancelArgs.self) + + if let notifications = args.notifications { + UNUserNotificationCenter.current().removePendingNotificationRequests( + withIdentifiers: notifications.map { String($0) } + ) + } else { + UNUserNotificationCenter.current().removeAllPendingNotificationRequests() + } + invoke.resolve() + } + + @objc func getPending(_ invoke: Invoke) { + UNUserNotificationCenter.current().getPendingNotificationRequests(completionHandler: { + (notifications) in + let ret = notifications.compactMap({ [weak self] (notification) -> PendingNotification? in + return self?.notificationHandler.toPendingNotification(notification) + }) + + invoke.resolve(ret) + }) + } + + @objc func registerActionTypes(_ invoke: Invoke) throws { + let args = try invoke.parseArgs(RegisterActionTypesArgs.self) + makeCategories(args.types) + invoke.resolve() + } + + @objc func removeActive(_ invoke: Invoke) { + do { + let args = try invoke.parseArgs(RemoveActiveArgs.self) + UNUserNotificationCenter.current().removeDeliveredNotifications( + withIdentifiers: args.notifications.map { String($0.id) }) + invoke.resolve() + } catch { + UNUserNotificationCenter.current().removeAllDeliveredNotifications() + DispatchQueue.main.async(execute: { + UIApplication.shared.applicationIconBadgeNumber = 0 + }) + invoke.resolve() + } + } + + @objc func getActive(_ invoke: Invoke) { + UNUserNotificationCenter.current().getDeliveredNotifications(completionHandler: { + (notifications) in + let ret = notifications.map({ (notification) -> ActiveNotification in + return self.notificationHandler.toActiveNotification( + notification.request) + }) + invoke.resolve(ret) + }) + } + + @objc func createChannel(_ invoke: Invoke) { + invoke.reject("not implemented") + } + + @objc func deleteChannel(_ invoke: Invoke) { + invoke.reject("not implemented") + } + + @objc func listChannels(_ invoke: Invoke) { + invoke.reject("not implemented") + } + +} + +@_cdecl("init_plugin_notification") +func initPlugin() -> Plugin { + return NotificationPlugin() +} diff --git a/plugins/notification/ios/Tests/PluginTests/PluginTests.swift b/plugins/notification/ios/Tests/PluginTests/PluginTests.swift new file mode 100644 index 0000000..99992ce --- /dev/null +++ b/plugins/notification/ios/Tests/PluginTests/PluginTests.swift @@ -0,0 +1,12 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +import XCTest +@testable import ExamplePlugin + +final class ExamplePluginTests: XCTestCase { + func testExample() throws { + let plugin = ExamplePlugin() + } +} diff --git a/plugins/notification/package.json b/plugins/notification/package.json new file mode 100644 index 0000000..58d64a0 --- /dev/null +++ b/plugins/notification/package.json @@ -0,0 +1,29 @@ +{ + "name": "@tauri-apps/plugin-notification", + "version": "2.4.0", + "license": "MIT OR Apache-2.0", + "authors": [ + "Tauri Programme within The Commons Conservancy" + ], + "repository": "https://github.com/tauri-apps/plugins-workspace", + "type": "module", + "types": "./dist-js/index.d.ts", + "main": "./dist-js/index.cjs", + "module": "./dist-js/index.js", + "exports": { + "types": "./dist-js/index.d.ts", + "import": "./dist-js/index.js", + "require": "./dist-js/index.cjs" + }, + "scripts": { + "build": "rollup -c" + }, + "files": [ + "dist-js", + "README.md", + "LICENSE" + ], + "dependencies": { + "@tauri-apps/api": "^2.11.0" + } +} diff --git a/plugins/notification/permissions/autogenerated/commands/batch.toml b/plugins/notification/permissions/autogenerated/commands/batch.toml new file mode 100644 index 0000000..c52cc16 --- /dev/null +++ b/plugins/notification/permissions/autogenerated/commands/batch.toml @@ -0,0 +1,13 @@ +# Automatically generated - DO NOT EDIT! + +"$schema" = "../../schemas/schema.json" + +[[permission]] +identifier = "allow-batch" +description = "Enables the batch command without any pre-configured scope." +commands.allow = ["batch"] + +[[permission]] +identifier = "deny-batch" +description = "Denies the batch command without any pre-configured scope." +commands.deny = ["batch"] diff --git a/plugins/notification/permissions/autogenerated/commands/cancel.toml b/plugins/notification/permissions/autogenerated/commands/cancel.toml new file mode 100644 index 0000000..91efeaa --- /dev/null +++ b/plugins/notification/permissions/autogenerated/commands/cancel.toml @@ -0,0 +1,13 @@ +# Automatically generated - DO NOT EDIT! + +"$schema" = "../../schemas/schema.json" + +[[permission]] +identifier = "allow-cancel" +description = "Enables the cancel command without any pre-configured scope." +commands.allow = ["cancel"] + +[[permission]] +identifier = "deny-cancel" +description = "Denies the cancel command without any pre-configured scope." +commands.deny = ["cancel"] diff --git a/plugins/notification/permissions/autogenerated/commands/check_permissions.toml b/plugins/notification/permissions/autogenerated/commands/check_permissions.toml new file mode 100644 index 0000000..f5af08b --- /dev/null +++ b/plugins/notification/permissions/autogenerated/commands/check_permissions.toml @@ -0,0 +1,13 @@ +# Automatically generated - DO NOT EDIT! + +"$schema" = "../../schemas/schema.json" + +[[permission]] +identifier = "allow-check-permissions" +description = "Enables the check_permissions command without any pre-configured scope." +commands.allow = ["check_permissions"] + +[[permission]] +identifier = "deny-check-permissions" +description = "Denies the check_permissions command without any pre-configured scope." +commands.deny = ["check_permissions"] diff --git a/plugins/notification/permissions/autogenerated/commands/create_channel.toml b/plugins/notification/permissions/autogenerated/commands/create_channel.toml new file mode 100644 index 0000000..2c93147 --- /dev/null +++ b/plugins/notification/permissions/autogenerated/commands/create_channel.toml @@ -0,0 +1,13 @@ +# Automatically generated - DO NOT EDIT! + +"$schema" = "../../schemas/schema.json" + +[[permission]] +identifier = "allow-create-channel" +description = "Enables the create_channel command without any pre-configured scope." +commands.allow = ["create_channel"] + +[[permission]] +identifier = "deny-create-channel" +description = "Denies the create_channel command without any pre-configured scope." +commands.deny = ["create_channel"] diff --git a/plugins/notification/permissions/autogenerated/commands/delete_channel.toml b/plugins/notification/permissions/autogenerated/commands/delete_channel.toml new file mode 100644 index 0000000..0adaf2b --- /dev/null +++ b/plugins/notification/permissions/autogenerated/commands/delete_channel.toml @@ -0,0 +1,13 @@ +# Automatically generated - DO NOT EDIT! + +"$schema" = "../../schemas/schema.json" + +[[permission]] +identifier = "allow-delete-channel" +description = "Enables the delete_channel command without any pre-configured scope." +commands.allow = ["delete_channel"] + +[[permission]] +identifier = "deny-delete-channel" +description = "Denies the delete_channel command without any pre-configured scope." +commands.deny = ["delete_channel"] diff --git a/plugins/notification/permissions/autogenerated/commands/get_active.toml b/plugins/notification/permissions/autogenerated/commands/get_active.toml new file mode 100644 index 0000000..b841eb8 --- /dev/null +++ b/plugins/notification/permissions/autogenerated/commands/get_active.toml @@ -0,0 +1,13 @@ +# Automatically generated - DO NOT EDIT! + +"$schema" = "../../schemas/schema.json" + +[[permission]] +identifier = "allow-get-active" +description = "Enables the get_active command without any pre-configured scope." +commands.allow = ["get_active"] + +[[permission]] +identifier = "deny-get-active" +description = "Denies the get_active command without any pre-configured scope." +commands.deny = ["get_active"] diff --git a/plugins/notification/permissions/autogenerated/commands/get_pending.toml b/plugins/notification/permissions/autogenerated/commands/get_pending.toml new file mode 100644 index 0000000..f3bae7a --- /dev/null +++ b/plugins/notification/permissions/autogenerated/commands/get_pending.toml @@ -0,0 +1,13 @@ +# Automatically generated - DO NOT EDIT! + +"$schema" = "../../schemas/schema.json" + +[[permission]] +identifier = "allow-get-pending" +description = "Enables the get_pending command without any pre-configured scope." +commands.allow = ["get_pending"] + +[[permission]] +identifier = "deny-get-pending" +description = "Denies the get_pending command without any pre-configured scope." +commands.deny = ["get_pending"] diff --git a/plugins/notification/permissions/autogenerated/commands/is_permission_granted.toml b/plugins/notification/permissions/autogenerated/commands/is_permission_granted.toml new file mode 100644 index 0000000..5faa73f --- /dev/null +++ b/plugins/notification/permissions/autogenerated/commands/is_permission_granted.toml @@ -0,0 +1,13 @@ +# Automatically generated - DO NOT EDIT! + +"$schema" = "../../schemas/schema.json" + +[[permission]] +identifier = "allow-is-permission-granted" +description = "Enables the is_permission_granted command without any pre-configured scope." +commands.allow = ["is_permission_granted"] + +[[permission]] +identifier = "deny-is-permission-granted" +description = "Denies the is_permission_granted command without any pre-configured scope." +commands.deny = ["is_permission_granted"] diff --git a/plugins/notification/permissions/autogenerated/commands/list_channels.toml b/plugins/notification/permissions/autogenerated/commands/list_channels.toml new file mode 100644 index 0000000..cb20cd5 --- /dev/null +++ b/plugins/notification/permissions/autogenerated/commands/list_channels.toml @@ -0,0 +1,13 @@ +# Automatically generated - DO NOT EDIT! + +"$schema" = "../../schemas/schema.json" + +[[permission]] +identifier = "allow-list-channels" +description = "Enables the list_channels command without any pre-configured scope." +commands.allow = ["list_channels"] + +[[permission]] +identifier = "deny-list-channels" +description = "Denies the list_channels command without any pre-configured scope." +commands.deny = ["list_channels"] diff --git a/plugins/notification/permissions/autogenerated/commands/notify.toml b/plugins/notification/permissions/autogenerated/commands/notify.toml new file mode 100644 index 0000000..7d6d46c --- /dev/null +++ b/plugins/notification/permissions/autogenerated/commands/notify.toml @@ -0,0 +1,13 @@ +# Automatically generated - DO NOT EDIT! + +"$schema" = "../../schemas/schema.json" + +[[permission]] +identifier = "allow-notify" +description = "Enables the notify command without any pre-configured scope." +commands.allow = ["notify"] + +[[permission]] +identifier = "deny-notify" +description = "Denies the notify command without any pre-configured scope." +commands.deny = ["notify"] diff --git a/plugins/notification/permissions/autogenerated/commands/permission_state.toml b/plugins/notification/permissions/autogenerated/commands/permission_state.toml new file mode 100644 index 0000000..dddcd86 --- /dev/null +++ b/plugins/notification/permissions/autogenerated/commands/permission_state.toml @@ -0,0 +1,13 @@ +# Automatically generated - DO NOT EDIT! + +"$schema" = "../../schemas/schema.json" + +[[permission]] +identifier = "allow-permission-state" +description = "Enables the permission_state command without any pre-configured scope." +commands.allow = ["permission_state"] + +[[permission]] +identifier = "deny-permission-state" +description = "Denies the permission_state command without any pre-configured scope." +commands.deny = ["permission_state"] diff --git a/plugins/notification/permissions/autogenerated/commands/register_action_types.toml b/plugins/notification/permissions/autogenerated/commands/register_action_types.toml new file mode 100644 index 0000000..cb5aa89 --- /dev/null +++ b/plugins/notification/permissions/autogenerated/commands/register_action_types.toml @@ -0,0 +1,13 @@ +# Automatically generated - DO NOT EDIT! + +"$schema" = "../../schemas/schema.json" + +[[permission]] +identifier = "allow-register-action-types" +description = "Enables the register_action_types command without any pre-configured scope." +commands.allow = ["register_action_types"] + +[[permission]] +identifier = "deny-register-action-types" +description = "Denies the register_action_types command without any pre-configured scope." +commands.deny = ["register_action_types"] diff --git a/plugins/notification/permissions/autogenerated/commands/register_listener.toml b/plugins/notification/permissions/autogenerated/commands/register_listener.toml new file mode 100644 index 0000000..48363c0 --- /dev/null +++ b/plugins/notification/permissions/autogenerated/commands/register_listener.toml @@ -0,0 +1,13 @@ +# Automatically generated - DO NOT EDIT! + +"$schema" = "../../schemas/schema.json" + +[[permission]] +identifier = "allow-register-listener" +description = "Enables the register_listener command without any pre-configured scope." +commands.allow = ["register_listener"] + +[[permission]] +identifier = "deny-register-listener" +description = "Denies the register_listener command without any pre-configured scope." +commands.deny = ["register_listener"] diff --git a/plugins/notification/permissions/autogenerated/commands/remove_active.toml b/plugins/notification/permissions/autogenerated/commands/remove_active.toml new file mode 100644 index 0000000..9ad2add --- /dev/null +++ b/plugins/notification/permissions/autogenerated/commands/remove_active.toml @@ -0,0 +1,13 @@ +# Automatically generated - DO NOT EDIT! + +"$schema" = "../../schemas/schema.json" + +[[permission]] +identifier = "allow-remove-active" +description = "Enables the remove_active command without any pre-configured scope." +commands.allow = ["remove_active"] + +[[permission]] +identifier = "deny-remove-active" +description = "Denies the remove_active command without any pre-configured scope." +commands.deny = ["remove_active"] diff --git a/plugins/notification/permissions/autogenerated/commands/remove_listener.toml b/plugins/notification/permissions/autogenerated/commands/remove_listener.toml new file mode 100644 index 0000000..9f315e5 --- /dev/null +++ b/plugins/notification/permissions/autogenerated/commands/remove_listener.toml @@ -0,0 +1,13 @@ +# Automatically generated - DO NOT EDIT! + +"$schema" = "../../schemas/schema.json" + +[[permission]] +identifier = "allow-remove-listener" +description = "Enables the remove_listener command without any pre-configured scope." +commands.allow = ["remove_listener"] + +[[permission]] +identifier = "deny-remove-listener" +description = "Denies the remove_listener command without any pre-configured scope." +commands.deny = ["remove_listener"] diff --git a/plugins/notification/permissions/autogenerated/commands/request_permission.toml b/plugins/notification/permissions/autogenerated/commands/request_permission.toml new file mode 100644 index 0000000..0b410d6 --- /dev/null +++ b/plugins/notification/permissions/autogenerated/commands/request_permission.toml @@ -0,0 +1,13 @@ +# Automatically generated - DO NOT EDIT! + +"$schema" = "../../schemas/schema.json" + +[[permission]] +identifier = "allow-request-permission" +description = "Enables the request_permission command without any pre-configured scope." +commands.allow = ["request_permission"] + +[[permission]] +identifier = "deny-request-permission" +description = "Denies the request_permission command without any pre-configured scope." +commands.deny = ["request_permission"] diff --git a/plugins/notification/permissions/autogenerated/commands/show.toml b/plugins/notification/permissions/autogenerated/commands/show.toml new file mode 100644 index 0000000..3d4cbf3 --- /dev/null +++ b/plugins/notification/permissions/autogenerated/commands/show.toml @@ -0,0 +1,13 @@ +# Automatically generated - DO NOT EDIT! + +"$schema" = "../../schemas/schema.json" + +[[permission]] +identifier = "allow-show" +description = "Enables the show command without any pre-configured scope." +commands.allow = ["show"] + +[[permission]] +identifier = "deny-show" +description = "Denies the show command without any pre-configured scope." +commands.deny = ["show"] diff --git a/plugins/notification/permissions/autogenerated/reference.md b/plugins/notification/permissions/autogenerated/reference.md new file mode 100644 index 0000000..c79a441 --- /dev/null +++ b/plugins/notification/permissions/autogenerated/reference.md @@ -0,0 +1,480 @@ +## Default Permission + +This permission set configures which +notification features are by default exposed. + +#### Granted Permissions + +It allows all notification related features. + +#### This default permission set includes the following: + +- `allow-is-permission-granted` +- `allow-request-permission` +- `allow-notify` +- `allow-register-action-types` +- `allow-register-listener` +- `allow-remove-listener` +- `allow-cancel` +- `allow-get-pending` +- `allow-remove-active` +- `allow-get-active` +- `allow-check-permissions` +- `allow-show` +- `allow-batch` +- `allow-list-channels` +- `allow-delete-channel` +- `allow-create-channel` +- `allow-permission-state` + +## Permission Table + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
IdentifierDescription
+ +`notification:allow-batch` + + + +Enables the batch command without any pre-configured scope. + +
+ +`notification:deny-batch` + + + +Denies the batch command without any pre-configured scope. + +
+ +`notification:allow-cancel` + + + +Enables the cancel command without any pre-configured scope. + +
+ +`notification:deny-cancel` + + + +Denies the cancel command without any pre-configured scope. + +
+ +`notification:allow-check-permissions` + + + +Enables the check_permissions command without any pre-configured scope. + +
+ +`notification:deny-check-permissions` + + + +Denies the check_permissions command without any pre-configured scope. + +
+ +`notification:allow-create-channel` + + + +Enables the create_channel command without any pre-configured scope. + +
+ +`notification:deny-create-channel` + + + +Denies the create_channel command without any pre-configured scope. + +
+ +`notification:allow-delete-channel` + + + +Enables the delete_channel command without any pre-configured scope. + +
+ +`notification:deny-delete-channel` + + + +Denies the delete_channel command without any pre-configured scope. + +
+ +`notification:allow-get-active` + + + +Enables the get_active command without any pre-configured scope. + +
+ +`notification:deny-get-active` + + + +Denies the get_active command without any pre-configured scope. + +
+ +`notification:allow-get-pending` + + + +Enables the get_pending command without any pre-configured scope. + +
+ +`notification:deny-get-pending` + + + +Denies the get_pending command without any pre-configured scope. + +
+ +`notification:allow-is-permission-granted` + + + +Enables the is_permission_granted command without any pre-configured scope. + +
+ +`notification:deny-is-permission-granted` + + + +Denies the is_permission_granted command without any pre-configured scope. + +
+ +`notification:allow-list-channels` + + + +Enables the list_channels command without any pre-configured scope. + +
+ +`notification:deny-list-channels` + + + +Denies the list_channels command without any pre-configured scope. + +
+ +`notification:allow-notify` + + + +Enables the notify command without any pre-configured scope. + +
+ +`notification:deny-notify` + + + +Denies the notify command without any pre-configured scope. + +
+ +`notification:allow-permission-state` + + + +Enables the permission_state command without any pre-configured scope. + +
+ +`notification:deny-permission-state` + + + +Denies the permission_state command without any pre-configured scope. + +
+ +`notification:allow-register-action-types` + + + +Enables the register_action_types command without any pre-configured scope. + +
+ +`notification:deny-register-action-types` + + + +Denies the register_action_types command without any pre-configured scope. + +
+ +`notification:allow-register-listener` + + + +Enables the register_listener command without any pre-configured scope. + +
+ +`notification:deny-register-listener` + + + +Denies the register_listener command without any pre-configured scope. + +
+ +`notification:allow-remove-active` + + + +Enables the remove_active command without any pre-configured scope. + +
+ +`notification:deny-remove-active` + + + +Denies the remove_active command without any pre-configured scope. + +
+ +`notification:allow-remove-listener` + + + +Enables the remove_listener command without any pre-configured scope. + +
+ +`notification:deny-remove-listener` + + + +Denies the remove_listener command without any pre-configured scope. + +
+ +`notification:allow-request-permission` + + + +Enables the request_permission command without any pre-configured scope. + +
+ +`notification:deny-request-permission` + + + +Denies the request_permission command without any pre-configured scope. + +
+ +`notification:allow-show` + + + +Enables the show command without any pre-configured scope. + +
+ +`notification:deny-show` + + + +Denies the show command without any pre-configured scope. + +
diff --git a/plugins/notification/permissions/default.toml b/plugins/notification/permissions/default.toml new file mode 100644 index 0000000..76d9e27 --- /dev/null +++ b/plugins/notification/permissions/default.toml @@ -0,0 +1,31 @@ +"$schema" = "schemas/schema.json" +[default] +description = """ +This permission set configures which +notification features are by default exposed. + +#### Granted Permissions + +It allows all notification related features. + +""" + +permissions = [ + "allow-is-permission-granted", + "allow-request-permission", + "allow-notify", + "allow-register-action-types", + "allow-register-listener", + "allow-remove-listener", + "allow-cancel", + "allow-get-pending", + "allow-remove-active", + "allow-get-active", + "allow-check-permissions", + "allow-show", + "allow-batch", + "allow-list-channels", + "allow-delete-channel", + "allow-create-channel", + "allow-permission-state", +] diff --git a/plugins/notification/permissions/schemas/schema.json b/plugins/notification/permissions/schemas/schema.json new file mode 100644 index 0000000..bea41b7 --- /dev/null +++ b/plugins/notification/permissions/schemas/schema.json @@ -0,0 +1,510 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "PermissionFile", + "description": "Permission file that can define a default permission, a set of permissions or a list of inlined permissions.", + "type": "object", + "properties": { + "default": { + "description": "The default permission set for the plugin", + "anyOf": [ + { + "$ref": "#/definitions/DefaultPermission" + }, + { + "type": "null" + } + ] + }, + "set": { + "description": "A list of permissions sets defined", + "type": "array", + "items": { + "$ref": "#/definitions/PermissionSet" + } + }, + "permission": { + "description": "A list of inlined permissions", + "default": [], + "type": "array", + "items": { + "$ref": "#/definitions/Permission" + } + } + }, + "definitions": { + "DefaultPermission": { + "description": "The default permission set of the plugin.\n\nWorks similarly to a permission with the \"default\" identifier.", + "type": "object", + "required": [ + "permissions" + ], + "properties": { + "version": { + "description": "The version of the permission.", + "type": [ + "integer", + "null" + ], + "format": "uint64", + "minimum": 1.0 + }, + "description": { + "description": "Human-readable description of what the permission does. Tauri convention is to use `

` headings in markdown content for Tauri documentation generation purposes.", + "type": [ + "string", + "null" + ] + }, + "permissions": { + "description": "All permissions this set contains.", + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "PermissionSet": { + "description": "A set of direct permissions grouped together under a new name.", + "type": "object", + "required": [ + "description", + "identifier", + "permissions" + ], + "properties": { + "identifier": { + "description": "A unique identifier for the permission.", + "type": "string" + }, + "description": { + "description": "Human-readable description of what the permission does.", + "type": "string" + }, + "permissions": { + "description": "All permissions this set contains.", + "type": "array", + "items": { + "$ref": "#/definitions/PermissionKind" + } + } + } + }, + "Permission": { + "description": "Descriptions of explicit privileges of commands.\n\nIt can enable commands to be accessible in the frontend of the application.\n\nIf the scope is defined it can be used to fine grain control the access of individual or multiple commands.", + "type": "object", + "required": [ + "identifier" + ], + "properties": { + "version": { + "description": "The version of the permission.", + "type": [ + "integer", + "null" + ], + "format": "uint64", + "minimum": 1.0 + }, + "identifier": { + "description": "A unique identifier for the permission.", + "type": "string" + }, + "description": { + "description": "Human-readable description of what the permission does. Tauri internal convention is to use `

` headings in markdown content for Tauri documentation generation purposes.", + "type": [ + "string", + "null" + ] + }, + "commands": { + "description": "Allowed or denied commands when using this permission.", + "default": { + "allow": [], + "deny": [] + }, + "allOf": [ + { + "$ref": "#/definitions/Commands" + } + ] + }, + "scope": { + "description": "Allowed or denied scoped when using this permission.", + "allOf": [ + { + "$ref": "#/definitions/Scopes" + } + ] + }, + "platforms": { + "description": "Target platforms this permission applies. By default all platforms are affected by this permission.", + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/Target" + } + } + } + }, + "Commands": { + "description": "Allowed and denied commands inside a permission.\n\nIf two commands clash inside of `allow` and `deny`, it should be denied by default.", + "type": "object", + "properties": { + "allow": { + "description": "Allowed command.", + "default": [], + "type": "array", + "items": { + "type": "string" + } + }, + "deny": { + "description": "Denied command, which takes priority.", + "default": [], + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "Scopes": { + "description": "An argument for fine grained behavior control of Tauri commands.\n\nIt can be of any serde serializable type and is used to allow or prevent certain actions inside a Tauri command. The configured scope is passed to the command and will be enforced by the command implementation.\n\n## Example\n\n```json { \"allow\": [{ \"path\": \"$HOME/**\" }], \"deny\": [{ \"path\": \"$HOME/secret.txt\" }] } ```", + "type": "object", + "properties": { + "allow": { + "description": "Data that defines what is allowed by the scope.", + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/Value" + } + }, + "deny": { + "description": "Data that defines what is denied by the scope. This should be prioritized by validation logic.", + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/Value" + } + } + } + }, + "Value": { + "description": "All supported ACL values.", + "anyOf": [ + { + "description": "Represents a null JSON value.", + "type": "null" + }, + { + "description": "Represents a [`bool`].", + "type": "boolean" + }, + { + "description": "Represents a valid ACL [`Number`].", + "allOf": [ + { + "$ref": "#/definitions/Number" + } + ] + }, + { + "description": "Represents a [`String`].", + "type": "string" + }, + { + "description": "Represents a list of other [`Value`]s.", + "type": "array", + "items": { + "$ref": "#/definitions/Value" + } + }, + { + "description": "Represents a map of [`String`] keys to [`Value`]s.", + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/Value" + } + } + ] + }, + "Number": { + "description": "A valid ACL number.", + "anyOf": [ + { + "description": "Represents an [`i64`].", + "type": "integer", + "format": "int64" + }, + { + "description": "Represents a [`f64`].", + "type": "number", + "format": "double" + } + ] + }, + "Target": { + "description": "Platform target.", + "oneOf": [ + { + "description": "MacOS.", + "type": "string", + "enum": [ + "macOS" + ] + }, + { + "description": "Windows.", + "type": "string", + "enum": [ + "windows" + ] + }, + { + "description": "Linux.", + "type": "string", + "enum": [ + "linux" + ] + }, + { + "description": "Android.", + "type": "string", + "enum": [ + "android" + ] + }, + { + "description": "iOS.", + "type": "string", + "enum": [ + "iOS" + ] + } + ] + }, + "PermissionKind": { + "type": "string", + "oneOf": [ + { + "description": "Enables the batch command without any pre-configured scope.", + "type": "string", + "const": "allow-batch", + "markdownDescription": "Enables the batch command without any pre-configured scope." + }, + { + "description": "Denies the batch command without any pre-configured scope.", + "type": "string", + "const": "deny-batch", + "markdownDescription": "Denies the batch command without any pre-configured scope." + }, + { + "description": "Enables the cancel command without any pre-configured scope.", + "type": "string", + "const": "allow-cancel", + "markdownDescription": "Enables the cancel command without any pre-configured scope." + }, + { + "description": "Denies the cancel command without any pre-configured scope.", + "type": "string", + "const": "deny-cancel", + "markdownDescription": "Denies the cancel command without any pre-configured scope." + }, + { + "description": "Enables the check_permissions command without any pre-configured scope.", + "type": "string", + "const": "allow-check-permissions", + "markdownDescription": "Enables the check_permissions command without any pre-configured scope." + }, + { + "description": "Denies the check_permissions command without any pre-configured scope.", + "type": "string", + "const": "deny-check-permissions", + "markdownDescription": "Denies the check_permissions command without any pre-configured scope." + }, + { + "description": "Enables the create_channel command without any pre-configured scope.", + "type": "string", + "const": "allow-create-channel", + "markdownDescription": "Enables the create_channel command without any pre-configured scope." + }, + { + "description": "Denies the create_channel command without any pre-configured scope.", + "type": "string", + "const": "deny-create-channel", + "markdownDescription": "Denies the create_channel command without any pre-configured scope." + }, + { + "description": "Enables the delete_channel command without any pre-configured scope.", + "type": "string", + "const": "allow-delete-channel", + "markdownDescription": "Enables the delete_channel command without any pre-configured scope." + }, + { + "description": "Denies the delete_channel command without any pre-configured scope.", + "type": "string", + "const": "deny-delete-channel", + "markdownDescription": "Denies the delete_channel command without any pre-configured scope." + }, + { + "description": "Enables the get_active command without any pre-configured scope.", + "type": "string", + "const": "allow-get-active", + "markdownDescription": "Enables the get_active command without any pre-configured scope." + }, + { + "description": "Denies the get_active command without any pre-configured scope.", + "type": "string", + "const": "deny-get-active", + "markdownDescription": "Denies the get_active command without any pre-configured scope." + }, + { + "description": "Enables the get_pending command without any pre-configured scope.", + "type": "string", + "const": "allow-get-pending", + "markdownDescription": "Enables the get_pending command without any pre-configured scope." + }, + { + "description": "Denies the get_pending command without any pre-configured scope.", + "type": "string", + "const": "deny-get-pending", + "markdownDescription": "Denies the get_pending command without any pre-configured scope." + }, + { + "description": "Enables the is_permission_granted command without any pre-configured scope.", + "type": "string", + "const": "allow-is-permission-granted", + "markdownDescription": "Enables the is_permission_granted command without any pre-configured scope." + }, + { + "description": "Denies the is_permission_granted command without any pre-configured scope.", + "type": "string", + "const": "deny-is-permission-granted", + "markdownDescription": "Denies the is_permission_granted command without any pre-configured scope." + }, + { + "description": "Enables the list_channels command without any pre-configured scope.", + "type": "string", + "const": "allow-list-channels", + "markdownDescription": "Enables the list_channels command without any pre-configured scope." + }, + { + "description": "Denies the list_channels command without any pre-configured scope.", + "type": "string", + "const": "deny-list-channels", + "markdownDescription": "Denies the list_channels command without any pre-configured scope." + }, + { + "description": "Enables the notify command without any pre-configured scope.", + "type": "string", + "const": "allow-notify", + "markdownDescription": "Enables the notify command without any pre-configured scope." + }, + { + "description": "Denies the notify command without any pre-configured scope.", + "type": "string", + "const": "deny-notify", + "markdownDescription": "Denies the notify command without any pre-configured scope." + }, + { + "description": "Enables the permission_state command without any pre-configured scope.", + "type": "string", + "const": "allow-permission-state", + "markdownDescription": "Enables the permission_state command without any pre-configured scope." + }, + { + "description": "Denies the permission_state command without any pre-configured scope.", + "type": "string", + "const": "deny-permission-state", + "markdownDescription": "Denies the permission_state command without any pre-configured scope." + }, + { + "description": "Enables the register_action_types command without any pre-configured scope.", + "type": "string", + "const": "allow-register-action-types", + "markdownDescription": "Enables the register_action_types command without any pre-configured scope." + }, + { + "description": "Denies the register_action_types command without any pre-configured scope.", + "type": "string", + "const": "deny-register-action-types", + "markdownDescription": "Denies the register_action_types command without any pre-configured scope." + }, + { + "description": "Enables the register_listener command without any pre-configured scope.", + "type": "string", + "const": "allow-register-listener", + "markdownDescription": "Enables the register_listener command without any pre-configured scope." + }, + { + "description": "Denies the register_listener command without any pre-configured scope.", + "type": "string", + "const": "deny-register-listener", + "markdownDescription": "Denies the register_listener command without any pre-configured scope." + }, + { + "description": "Enables the remove_active command without any pre-configured scope.", + "type": "string", + "const": "allow-remove-active", + "markdownDescription": "Enables the remove_active command without any pre-configured scope." + }, + { + "description": "Denies the remove_active command without any pre-configured scope.", + "type": "string", + "const": "deny-remove-active", + "markdownDescription": "Denies the remove_active command without any pre-configured scope." + }, + { + "description": "Enables the remove_listener command without any pre-configured scope.", + "type": "string", + "const": "allow-remove-listener", + "markdownDescription": "Enables the remove_listener command without any pre-configured scope." + }, + { + "description": "Denies the remove_listener command without any pre-configured scope.", + "type": "string", + "const": "deny-remove-listener", + "markdownDescription": "Denies the remove_listener command without any pre-configured scope." + }, + { + "description": "Enables the request_permission command without any pre-configured scope.", + "type": "string", + "const": "allow-request-permission", + "markdownDescription": "Enables the request_permission command without any pre-configured scope." + }, + { + "description": "Denies the request_permission command without any pre-configured scope.", + "type": "string", + "const": "deny-request-permission", + "markdownDescription": "Denies the request_permission command without any pre-configured scope." + }, + { + "description": "Enables the show command without any pre-configured scope.", + "type": "string", + "const": "allow-show", + "markdownDescription": "Enables the show command without any pre-configured scope." + }, + { + "description": "Denies the show command without any pre-configured scope.", + "type": "string", + "const": "deny-show", + "markdownDescription": "Denies the show command without any pre-configured scope." + }, + { + "description": "This permission set configures which\nnotification features are by default exposed.\n\n#### Granted Permissions\n\nIt allows all notification related features.\n\n\n#### This default permission set includes:\n\n- `allow-is-permission-granted`\n- `allow-request-permission`\n- `allow-notify`\n- `allow-register-action-types`\n- `allow-register-listener`\n- `allow-remove-listener`\n- `allow-cancel`\n- `allow-get-pending`\n- `allow-remove-active`\n- `allow-get-active`\n- `allow-check-permissions`\n- `allow-show`\n- `allow-batch`\n- `allow-list-channels`\n- `allow-delete-channel`\n- `allow-create-channel`\n- `allow-permission-state`", + "type": "string", + "const": "default", + "markdownDescription": "This permission set configures which\nnotification features are by default exposed.\n\n#### Granted Permissions\n\nIt allows all notification related features.\n\n\n#### This default permission set includes:\n\n- `allow-is-permission-granted`\n- `allow-request-permission`\n- `allow-notify`\n- `allow-register-action-types`\n- `allow-register-listener`\n- `allow-remove-listener`\n- `allow-cancel`\n- `allow-get-pending`\n- `allow-remove-active`\n- `allow-get-active`\n- `allow-check-permissions`\n- `allow-show`\n- `allow-batch`\n- `allow-list-channels`\n- `allow-delete-channel`\n- `allow-create-channel`\n- `allow-permission-state`" + } + ] + } + } +} \ No newline at end of file diff --git a/plugins/notification/rollup.config.js b/plugins/notification/rollup.config.js new file mode 100644 index 0000000..a7dbd4f --- /dev/null +++ b/plugins/notification/rollup.config.js @@ -0,0 +1,22 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +import { createConfig } from '../../shared/rollup.config.js' +import { nodeResolve } from '@rollup/plugin-node-resolve' +import typescript from '@rollup/plugin-typescript' +import terser from '@rollup/plugin-terser' + +export default createConfig({ + additionalConfigs: { + input: 'guest-js/init.ts', + output: { + file: 'src/init-iife.js', + format: 'iife' + }, + plugins: [typescript(), terser(), nodeResolve()], + onwarn: (warning) => { + throw Object.assign(new Error(), warning) + } + } +}) diff --git a/plugins/notification/src/commands.rs b/plugins/notification/src/commands.rs new file mode 100644 index 0000000..99b96c5 --- /dev/null +++ b/plugins/notification/src/commands.rs @@ -0,0 +1,39 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use tauri::{command, plugin::PermissionState, AppHandle, Runtime, State}; + +use crate::{Notification, NotificationData, Result}; + +#[command] +pub(crate) async fn is_permission_granted( + _app: AppHandle, + notification: State<'_, Notification>, +) -> Result> { + let state = notification.permission_state()?; + match state { + PermissionState::Granted => Ok(Some(true)), + PermissionState::Denied => Ok(Some(false)), + PermissionState::Prompt | PermissionState::PromptWithRationale => Ok(None), + } +} + +#[command] +pub(crate) async fn request_permission( + _app: AppHandle, + notification: State<'_, Notification>, +) -> Result { + notification.request_permission() +} + +#[command] +pub(crate) async fn notify( + _app: AppHandle, + notification: State<'_, Notification>, + options: NotificationData, +) -> Result<()> { + let mut builder = notification.builder(); + builder.data = options; + builder.show() +} diff --git a/plugins/notification/src/desktop.rs b/plugins/notification/src/desktop.rs new file mode 100644 index 0000000..5b41a6c --- /dev/null +++ b/plugins/notification/src/desktop.rs @@ -0,0 +1,330 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use serde::de::DeserializeOwned; +use tauri::{ + plugin::{PermissionState, PluginApi}, + AppHandle, Runtime, +}; + +use crate::NotificationBuilder; + +/// Initializes the desktop implementation of the notification APIs. +pub fn init( + app: &AppHandle, + _api: PluginApi, +) -> crate::Result> { + Ok(Notification(app.clone())) +} + +/// Access to the notification APIs. +/// +/// You can get an instance of this type via [`NotificationExt`](crate::NotificationExt) +pub struct Notification(AppHandle); + +impl crate::NotificationBuilder { + /// Shows the notification. + /// + /// When no title was set with [`Self::title`], the `productName` from the Tauri configuration is used instead. + /// Only the title, body, icon and sound of the notification are used on desktop; + /// the scheduling, grouping and action related options are ignored. + /// + /// The notification is dispatched on a background task, so this returns as soon as the payload is prepared. + /// + /// # Errors + /// + /// Returns an error when the notification could not be prepared, + /// e.g. when the path of the running executable cannot be resolved on Windows. + /// + /// ## Platform-specific + /// + /// - **Windows**: Not supported on Windows 7 unless the `windows7-compat` Cargo feature is enabled. + pub fn show(self) -> crate::Result<()> { + let mut notification = imp::Notification::new(self.app.config().identifier.clone()); + + if let Some(title) = self + .data + .title + .or_else(|| self.app.config().product_name.clone()) + { + notification = notification.title(title); + } + if let Some(body) = self.data.body { + notification = notification.body(body); + } + if let Some(icon) = self.data.icon { + notification = notification.icon(icon); + } + if let Some(sound) = self.data.sound { + notification = notification.sound(sound); + } + #[cfg(feature = "windows7-compat")] + { + notification.notify(&self.app)?; + } + #[cfg(not(feature = "windows7-compat"))] + notification.show()?; + + Ok(()) + } +} + +impl Notification { + /// Creates a new builder for a notification. + /// + /// # Examples + /// + /// ```no_run + /// use tauri_plugin_notification::NotificationExt; + /// + /// fn notify(app: &tauri::AppHandle) { + /// app.notification() + /// .builder() + /// .title("Tauri") + /// .body("Tauri is awesome!") + /// .show() + /// .unwrap(); + /// } + /// ``` + pub fn builder(&self) -> NotificationBuilder { + NotificationBuilder::new(self.0.clone()) + } + + /// Requests the permission to send notifications. + /// + /// Desktop applications do not need to ask for this permission, + /// so this always resolves to [`PermissionState::Granted`] without prompting the user. + pub fn request_permission(&self) -> crate::Result { + Ok(PermissionState::Granted) + } + + /// Checks whether the permission to send notifications was granted. + /// + /// Desktop applications do not need to ask for this permission, + /// so this always resolves to [`PermissionState::Granted`]. + pub fn permission_state(&self) -> crate::Result { + Ok(PermissionState::Granted) + } +} + +mod imp { + //! Types and functions related to desktop notifications. + + #[cfg(windows)] + use std::path::MAIN_SEPARATOR as SEP; + + /// The desktop notification definition. + /// + /// Allows you to construct a Notification data and send it. + /// + /// # Examples + /// ```rust,no_run + /// use tauri_plugin_notification::NotificationExt; + /// // first we build the application to access the Tauri configuration + /// let app = tauri::Builder::default() + /// // on an actual app, remove the string argument + /// .build(tauri::generate_context!("test/tauri.conf.json")) + /// .expect("error while building tauri application"); + /// + /// // shows a notification with the given title and body + /// app.notification() + /// .builder() + /// .title("New message") + /// .body("You've got a new message.") + /// .show(); + /// + /// // run the app + /// app.run(|_app_handle, _event| {}); + /// ``` + #[allow(dead_code)] + #[derive(Debug, Default)] + pub struct Notification { + /// The notification body. + body: Option, + /// The notification title. + title: Option, + /// The notification icon. + icon: Option, + /// The notification sound. + sound: Option, + /// The notification identifier + identifier: String, + } + + impl Notification { + /// Initializes a instance of a Notification. + pub fn new(identifier: impl Into) -> Self { + Self { + identifier: identifier.into(), + ..Default::default() + } + } + + /// Sets the notification body. + #[must_use] + pub fn body(mut self, body: impl Into) -> Self { + self.body = Some(body.into()); + self + } + + /// Sets the notification title. + #[must_use] + pub fn title(mut self, title: impl Into) -> Self { + self.title = Some(title.into()); + self + } + + /// Sets the notification icon. + #[must_use] + pub fn icon(mut self, icon: impl Into) -> Self { + self.icon = Some(icon.into()); + self + } + + /// Sets the notification sound file. + #[must_use] + pub fn sound(mut self, sound: impl Into) -> Self { + self.sound = Some(sound.into()); + self + } + + /// Shows the notification. + /// + /// # Examples + /// + /// ```no_run + /// use tauri_plugin_notification::NotificationExt; + /// + /// tauri::Builder::default() + /// .setup(|app| { + /// app.notification() + /// .builder() + /// .title("Tauri") + /// .body("Tauri is awesome!") + /// .show() + /// .unwrap(); + /// Ok(()) + /// }) + /// .run(tauri::generate_context!("test/tauri.conf.json")) + /// .expect("error while running tauri application"); + /// ``` + /// + /// ## Platform-specific + /// + /// - **Windows**: Not supported on Windows 7. If your app targets it, enable the `windows7-compat` feature and use [`Self::notify`]. + #[cfg_attr( + all(not(docsrs), feature = "windows7-compat"), + deprecated = "This function does not work on Windows 7. Use `Self::notify` instead." + )] + pub fn show(self) -> crate::Result<()> { + let mut notification = notify_rust::Notification::new(); + if let Some(body) = self.body { + notification.body(&body); + } + if let Some(title) = self.title { + notification.summary(&title); + } + if let Some(icon) = self.icon { + notification.icon(&icon); + } else { + notification.auto_icon(); + } + if let Some(sound) = self.sound { + notification.sound_name(&sound); + } + #[cfg(windows)] + { + let exe = tauri::utils::platform::current_exe()?; + let exe_dir = exe.parent().expect("failed to get exe directory"); + let curr_dir = exe_dir.display().to_string(); + // set the notification's System.AppUserModel.ID only when running the installed app + if !(curr_dir.ends_with(format!("{SEP}target{SEP}debug").as_str()) + || curr_dir.ends_with(format!("{SEP}target{SEP}release").as_str())) + { + notification.app_id(&self.identifier); + } + } + #[cfg(target_os = "macos")] + { + let _ = notify_rust::set_application(if tauri::is_dev() { + "com.apple.Terminal" + } else { + &self.identifier + }); + } + + tauri::async_runtime::spawn(async move { + let _ = notification.show(); + }); + + Ok(()) + } + + /// Shows the notification. This API is similar to [`Self::show`], but it also works on Windows 7. + /// + /// # Examples + /// + /// ```no_run + /// use tauri_plugin_notification::NotificationExt; + /// + /// tauri::Builder::default() + /// .setup(move |app| { + /// app.notification().builder() + /// .title("Tauri") + /// .body("Tauri is awesome!") + /// .show() + /// .unwrap(); + /// Ok(()) + /// }) + /// .run(tauri::generate_context!("test/tauri.conf.json")) + /// .expect("error while running tauri application"); + /// ``` + #[cfg(feature = "windows7-compat")] + #[cfg_attr(docsrs, doc(cfg(feature = "windows7-compat")))] + #[allow(unused_variables)] + pub fn notify(self, app: &tauri::AppHandle) -> crate::Result<()> { + #[cfg(windows)] + { + fn is_windows_7() -> bool { + let v = windows_version::OsVersion::current(); + // windows 7 is 6.1 + v.major == 6 && v.minor == 1 + } + + if is_windows_7() { + self.notify_win7(app) + } else { + #[allow(deprecated)] + self.show() + } + } + #[cfg(not(windows))] + { + #[allow(deprecated)] + self.show() + } + } + + /// Shows the notification on Windows 7. + #[cfg(all(windows, feature = "windows7-compat"))] + fn notify_win7(self, app: &tauri::AppHandle) -> crate::Result<()> { + let app_ = app.clone(); + let _ = app.clone().run_on_main_thread(move || { + let mut notification = win7_notifications::Notification::new(); + if let Some(body) = self.body { + notification.body(&body); + } + if let Some(title) = self.title { + notification.summary(&title); + } + if let Some(icon) = app_.default_window_icon() { + notification.icon(icon.rgba().to_vec(), icon.width(), icon.height()); + } + let _ = notification.show(); + }); + + Ok(()) + } + } +} diff --git a/plugins/notification/src/error.rs b/plugins/notification/src/error.rs new file mode 100644 index 0000000..f43da58 --- /dev/null +++ b/plugins/notification/src/error.rs @@ -0,0 +1,33 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use serde::{ser::Serializer, Serialize}; + +/// Alias for a [`std::result::Result`] with the error type set to [`Error`]. +pub type Result = std::result::Result; + +/// Errors returned by the notification plugin. +/// +/// The error is serialized to its [`Display`](std::fmt::Display) string when it crosses the IPC boundary. +#[derive(Debug, thiserror::Error)] +pub enum Error { + /// An I/O operation failed, e.g. resolving the path of the running executable on Windows. + #[error(transparent)] + Io(#[from] std::io::Error), + /// Failed to run a command on the mobile plugin implementation (Kotlin on Android, Swift on iOS). + /// + /// Only available on mobile. + #[cfg(mobile)] + #[error(transparent)] + PluginInvoke(#[from] tauri::plugin::mobile::PluginInvokeError), +} + +impl Serialize for Error { + fn serialize(&self, serializer: S) -> std::result::Result + where + S: Serializer, + { + serializer.serialize_str(self.to_string().as_ref()) + } +} diff --git a/plugins/notification/src/init-iife.js b/plugins/notification/src/init-iife.js new file mode 100644 index 0000000..141e4a4 --- /dev/null +++ b/plugins/notification/src/init-iife.js @@ -0,0 +1 @@ +!function(){"use strict";async function i(i,n={},t){return window.__TAURI_INTERNALS__.invoke(i,n,t)}"function"==typeof SuppressedError&&SuppressedError,function(){let n=!1,t="default";function o(i){n=!0,window.Notification.permission=i,n=!1}window.Notification=function(n,t){const o=t||{};!async function(n){"object"==typeof n&&Object.freeze(n),await i("plugin:notification|notify",{options:"string"==typeof n?{title:n}:n})}(Object.assign(o,{title:n}))},window.Notification.requestPermission=async function(){return await i("plugin:notification|request_permission").then(i=>(o("prompt"===i||"prompt-with-rationale"===i?"default":i),i))},Object.defineProperty(window.Notification,"permission",{enumerable:!0,get:()=>t,set:i=>{if(!n)throw new Error("Readonly property");t=i}}),async function(){return"default"!==window.Notification.permission?await Promise.resolve("granted"===window.Notification.permission):!!__TEMPLATE_windows__||await i("plugin:notification|is_permission_granted")}().then(function(i){o(null===i?"default":i?"granted":"denied")})}()}(); diff --git a/plugins/notification/src/lib.rs b/plugins/notification/src/lib.rs new file mode 100644 index 0000000..32c2e61 --- /dev/null +++ b/plugins/notification/src/lib.rs @@ -0,0 +1,265 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +//! Send message notifications (brief auto-expiring OS window element) to your user. Can also be used with the Notification Web API. +//! +//! ## Cargo features +//! +//! - **windows7-compat**: Adds support for the legacy Windows 7 notification implementation and Windows-version detection. + +#![doc( + html_logo_url = "https://github.com/tauri-apps/tauri/raw/dev/app-icon.png", + html_favicon_url = "https://github.com/tauri-apps/tauri/raw/dev/app-icon.png" +)] + +use serde::Serialize; +#[cfg(mobile)] +use tauri::plugin::PluginHandle; +#[cfg(desktop)] +use tauri::AppHandle; +use tauri::{ + plugin::{Builder, TauriPlugin}, + Manager, Runtime, +}; + +pub use models::*; +pub use tauri::plugin::PermissionState; + +#[cfg(desktop)] +mod desktop; +#[cfg(mobile)] +mod mobile; + +mod commands; +mod error; +mod models; + +pub use error::{Error, Result}; + +#[cfg(desktop)] +pub use desktop::Notification; +#[cfg(mobile)] +pub use mobile::Notification; + +/// The notification builder. +#[derive(Debug)] +pub struct NotificationBuilder { + #[cfg(desktop)] + app: AppHandle, + #[cfg(mobile)] + handle: PluginHandle, + pub(crate) data: NotificationData, +} + +impl NotificationBuilder { + #[cfg(desktop)] + fn new(app: AppHandle) -> Self { + Self { + app, + data: Default::default(), + } + } + + #[cfg(mobile)] + fn new(handle: PluginHandle) -> Self { + Self { + handle, + data: Default::default(), + } + } + + /// Sets the notification identifier. + pub fn id(mut self, id: i32) -> Self { + self.data.id = id; + self + } + + /// Sets the identifier of the notification channel that delivers this notification. + /// + /// If the channel does not exist, the notification won't fire. + /// Make sure the channel exists with `Notification::list_channels` and + /// `Notification::create_channel`. + /// + /// Only used on Android. + pub fn channel_id(mut self, id: impl Into) -> Self { + self.data.channel_id.replace(id.into()); + self + } + + /// Sets the notification title. + pub fn title(mut self, title: impl Into) -> Self { + self.data.title.replace(title.into()); + self + } + + /// Sets the notification body. + pub fn body(mut self, body: impl Into) -> Self { + self.data.body.replace(body.into()); + self + } + + /// Schedule this notification to fire on a later time or a fixed interval. + pub fn schedule(mut self, schedule: Schedule) -> Self { + self.data.schedule.replace(schedule); + self + } + + /// Multiline text. + /// Changes the notification style to big text. + /// Cannot be used with `inboxLines`. + pub fn large_body(mut self, large_body: impl Into) -> Self { + self.data.large_body.replace(large_body.into()); + self + } + + /// Detail text for the notification with `largeBody`, `inboxLines` or `groupSummary`. + pub fn summary(mut self, summary: impl Into) -> Self { + self.data.summary.replace(summary.into()); + self + } + + /// Defines an action type for this notification. + pub fn action_type_id(mut self, action_type_id: impl Into) -> Self { + self.data.action_type_id.replace(action_type_id.into()); + self + } + + /// Identifier used to group multiple notifications. + /// + /// + pub fn group(mut self, group: impl Into) -> Self { + self.data.group.replace(group.into()); + self + } + + /// Instructs the system that this notification is the summary of a group on Android. + pub fn group_summary(mut self) -> Self { + self.data.group_summary = true; + self + } + + /// The sound resource name for the notification. + pub fn sound(mut self, sound: impl Into) -> Self { + self.data.sound.replace(sound.into()); + self + } + + /// Append an inbox line to the notification. + /// Changes the notification style to inbox. + /// Cannot be used with `largeBody`. + /// + /// Only supports up to 5 lines. + pub fn inbox_line(mut self, line: impl Into) -> Self { + self.data.inbox_lines.push(line.into()); + self + } + + /// Notification icon. + /// + /// On Android the icon must be placed in the app's `res/drawable` folder. + pub fn icon(mut self, icon: impl Into) -> Self { + self.data.icon.replace(icon.into()); + self + } + + /// Notification large icon (Android). + /// + /// The icon must be placed in the app's `res/drawable` folder. + pub fn large_icon(mut self, large_icon: impl Into) -> Self { + self.data.large_icon.replace(large_icon.into()); + self + } + + /// Icon color on Android. + pub fn icon_color(mut self, icon_color: impl Into) -> Self { + self.data.icon_color.replace(icon_color.into()); + self + } + + /// Append an attachment to the notification. + pub fn attachment(mut self, attachment: Attachment) -> Self { + self.data.attachments.push(attachment); + self + } + + /// Adds an extra payload to store in the notification. + pub fn extra(mut self, key: impl Into, value: impl Serialize) -> Self { + self.data + .extra + .insert(key.into(), serde_json::to_value(value).unwrap()); + self + } + + /// If true, the notification cannot be dismissed by the user on Android. + /// + /// An application service must manage the dismissal of the notification. + /// It is typically used to indicate a background task that is pending (e.g. a file download) + /// or the user is engaged with (e.g. playing music). + pub fn ongoing(mut self) -> Self { + self.data.ongoing = true; + self + } + + /// Automatically cancel the notification when the user clicks on it. + pub fn auto_cancel(mut self) -> Self { + self.data.auto_cancel = true; + self + } + + /// Changes the notification presentation to be silent on iOS (no badge, no sound, not listed). + pub fn silent(mut self) -> Self { + self.data.silent = true; + self + } +} + +/// Extensions to [`tauri::App`], [`tauri::AppHandle`], [`tauri::WebviewWindow`], [`tauri::Webview`] and [`tauri::Window`] to access the notification APIs. +pub trait NotificationExt { + /// Returns the notification APIs managed by the plugin. + /// + /// # Examples + /// + /// ```no_run + /// use tauri_plugin_notification::NotificationExt; + /// + /// fn notify(app: &tauri::AppHandle) { + /// app.notification() + /// .builder() + /// .title("Tauri") + /// .body("Tauri is awesome!") + /// .show() + /// .unwrap(); + /// } + /// ``` + fn notification(&self) -> &Notification; +} + +impl> crate::NotificationExt for T { + fn notification(&self) -> &Notification { + self.state::>().inner() + } +} + +/// Initializes the plugin. +pub fn init() -> TauriPlugin { + Builder::new("notification") + .invoke_handler(tauri::generate_handler![ + commands::notify, + commands::request_permission, + commands::is_permission_granted + ]) + .js_init_script(include_str!("init-iife.js").replace( + "__TEMPLATE_windows__", + if cfg!(windows) { "true" } else { "false" }, + )) + .setup(|app, api| { + #[cfg(mobile)] + let notification = mobile::init(app, api)?; + #[cfg(desktop)] + let notification = desktop::init(app, api)?; + app.manage(notification); + Ok(()) + }) + .build() +} diff --git a/plugins/notification/src/mobile.rs b/plugins/notification/src/mobile.rs new file mode 100644 index 0000000..250532d --- /dev/null +++ b/plugins/notification/src/mobile.rs @@ -0,0 +1,202 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use serde::{de::DeserializeOwned, Deserialize}; +use tauri::{ + plugin::{PermissionState, PluginApi, PluginHandle}, + AppHandle, Runtime, +}; + +use crate::models::*; + +use std::collections::HashMap; + +#[cfg(target_os = "android")] +const PLUGIN_IDENTIFIER: &str = "app.tauri.notification"; + +#[cfg(target_os = "ios")] +tauri::ios_plugin_binding!(init_plugin_notification); + +/// Initializes the mobile implementation of the notification APIs by registering +/// the Kotlin (Android) or Swift (iOS) plugin class. +pub fn init( + _app: &AppHandle, + api: PluginApi, +) -> crate::Result> { + #[cfg(target_os = "android")] + let handle = api.register_android_plugin(PLUGIN_IDENTIFIER, "NotificationPlugin")?; + #[cfg(target_os = "ios")] + let handle = api.register_ios_plugin(init_plugin_notification)?; + Ok(Notification(handle)) +} + +impl crate::NotificationBuilder { + /// Shows the notification, or schedules it when [`Self::schedule`] was called. + /// + /// # Errors + /// + /// Returns [`Error::PluginInvoke`](crate::Error::PluginInvoke) when the mobile plugin + /// rejects the notification, e.g. when the scheduled date is in the past. + pub fn show(self) -> crate::Result<()> { + self.handle + .run_mobile_plugin::("show", self.data) + .map(|_| ()) + .map_err(Into::into) + } +} + +/// Access to the notification APIs. +/// +/// You can get an instance of this type via [`NotificationExt`](crate::NotificationExt) +pub struct Notification(PluginHandle); + +impl Notification { + /// Creates a new builder for a notification. + /// + /// # Examples + /// + /// ```no_run + /// use tauri_plugin_notification::NotificationExt; + /// + /// fn notify(app: &tauri::AppHandle) { + /// app.notification() + /// .builder() + /// .title("Tauri") + /// .body("Tauri is awesome!") + /// .show() + /// .unwrap(); + /// } + /// ``` + pub fn builder(&self) -> crate::NotificationBuilder { + crate::NotificationBuilder::new(self.0.clone()) + } + + /// Requests the permission to send notifications, prompting the user when it was not decided yet. + /// + /// On Android this requests the `POST_NOTIFICATIONS` runtime permission. + pub fn request_permission(&self) -> crate::Result { + self.0 + .run_mobile_plugin::("requestPermissions", ()) + .map(|r| r.permission_state) + .map_err(Into::into) + } + + /// Checks the current state of the permission to send notifications without prompting the user. + pub fn permission_state(&self) -> crate::Result { + self.0 + .run_mobile_plugin::("checkPermissions", ()) + .map(|r| r.permission_state) + .map_err(Into::into) + } + + /// Registers the action types a notification can reference + /// through [`NotificationBuilder::action_type_id`](crate::NotificationBuilder::action_type_id). + /// + /// ## Platform-specific + /// + /// - **Android**: only the identifier, title and input flag of each [`Action`] are used. + /// - **iOS**: each action type is registered as a `UNNotificationCategory`. + pub fn register_action_types(&self, types: Vec) -> crate::Result<()> { + let mut args = HashMap::new(); + args.insert("types", types); + self.0 + .run_mobile_plugin("registerActionTypes", args) + .map_err(Into::into) + } + + /// Removes the delivered notifications with the given identifiers from the notification center. + /// + /// Use [`Self::remove_all_active`] to remove every delivered notification. + pub fn remove_active(&self, notifications: Vec) -> crate::Result<()> { + let mut args = HashMap::new(); + args.insert( + "notifications", + notifications + .into_iter() + .map(|id| { + let mut notification = HashMap::new(); + notification.insert("id", id); + notification + }) + .collect::>>(), + ); + self.0 + .run_mobile_plugin("removeActive", args) + .map_err(Into::into) + } + + /// Lists the notifications that were delivered and are still visible in the notification center. + pub fn active(&self) -> crate::Result> { + self.0 + .run_mobile_plugin("getActive", ()) + .map_err(Into::into) + } + + /// Removes all delivered notifications from the notification center. + pub fn remove_all_active(&self) -> crate::Result<()> { + self.0 + .run_mobile_plugin("removeActive", ()) + .map_err(Into::into) + } + + /// Lists the scheduled notifications that have not been delivered yet. + pub fn pending(&self) -> crate::Result> { + self.0 + .run_mobile_plugin("getPending", ()) + .map_err(Into::into) + } + + /// Cancel pending notifications. + pub fn cancel(&self, notifications: Vec) -> crate::Result<()> { + let mut args = HashMap::new(); + args.insert("notifications", notifications); + self.0.run_mobile_plugin("cancel", args).map_err(Into::into) + } + + /// Cancel all pending notifications. + pub fn cancel_all(&self) -> crate::Result<()> { + self.0.run_mobile_plugin("cancel", ()).map_err(Into::into) + } + + /// Creates a notification channel, which notifications can target + /// through [`NotificationBuilder::channel_id`](crate::NotificationBuilder::channel_id). + /// + /// Notifications that reference a channel that does not exist are not delivered. + /// + /// Only available on Android. + #[cfg(target_os = "android")] + pub fn create_channel(&self, channel: Channel) -> crate::Result<()> { + self.0 + .run_mobile_plugin("createChannel", channel) + .map_err(Into::into) + } + + /// Deletes the notification channel with the given identifier. + /// + /// Only available on Android. + #[cfg(target_os = "android")] + pub fn delete_channel(&self, id: impl Into) -> crate::Result<()> { + let mut args = HashMap::new(); + args.insert("id", id.into()); + self.0 + .run_mobile_plugin("deleteChannel", args) + .map_err(Into::into) + } + + /// Lists the notification channels that are currently registered for the app. + /// + /// Only available on Android. + #[cfg(target_os = "android")] + pub fn list_channels(&self) -> crate::Result> { + self.0 + .run_mobile_plugin("listChannels", ()) + .map_err(Into::into) + } +} + +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +struct PermissionResponse { + permission_state: PermissionState, +} diff --git a/plugins/notification/src/models.rs b/plugins/notification/src/models.rs new file mode 100644 index 0000000..ce5993a --- /dev/null +++ b/plugins/notification/src/models.rs @@ -0,0 +1,914 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use std::{collections::HashMap, fmt::Display}; + +use serde::{de::Error as DeError, Deserialize, Deserializer, Serialize, Serializer}; + +use url::Url; + +/// A media file attached to a notification. +/// +/// Attachments are only used on mobile; desktop notifications ignore them. +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct Attachment { + id: String, + url: Url, +} + +impl Attachment { + /// Creates a new attachment with the given identifier and URL. + /// + /// The URL accepts the `asset` and `file` protocols. + pub fn new(id: impl Into, url: Url) -> Self { + Self { id: id.into(), url } + } +} + +/// The set of date fields a notification must match to be delivered. +/// +/// Fields left as [`None`] match any value, so the notification fires on every date +/// whose remaining components match. Used by [`Schedule::Interval`]. +#[derive(Debug, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ScheduleInterval { + /// The year the notification fires on. + pub year: Option, + /// The month of the year the notification fires on. + pub month: Option, + /// The day of the month the notification fires on. + pub day: Option, + /// The day of the week the notification fires on. + /// + /// 1 - Sunday, 2 - Monday, 3 - Tuesday, 4 - Wednesday, 5 - Thursday, 6 - Friday, 7 - Saturday. + pub weekday: Option, + /// The hour of the day the notification fires on, in the 24-hour clock. + pub hour: Option, + /// The minute of the hour the notification fires on. + pub minute: Option, + /// The second of the minute the notification fires on. + pub second: Option, +} + +/// The unit of the repeating interval used by [`Schedule::Every`]. +/// +/// It is serialized as its lowercase camelCase name, e.g. `twoWeeks`. +#[derive(Debug)] +pub enum ScheduleEvery { + /// Repeats every year. + /// + /// On Android a year is approximated as 52 weeks. + Year, + /// Repeats every month. + /// + /// On Android a month is approximated as 30 days. + Month, + /// Repeats every two weeks. + TwoWeeks, + /// Repeats every week. + Week, + /// Repeats every day. + Day, + /// Repeats every hour. + Hour, + /// Repeats every minute. + Minute, + /// Repeats every second. + /// + /// Not supported on iOS, where repeating triggers must be at least a minute apart. + Second, +} + +impl Display for ScheduleEvery { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "{}", + match self { + Self::Year => "year", + Self::Month => "month", + Self::TwoWeeks => "twoWeeks", + Self::Week => "week", + Self::Day => "day", + Self::Hour => "hour", + Self::Minute => "minute", + Self::Second => "second", + } + ) + } +} + +impl Serialize for ScheduleEvery { + fn serialize(&self, serializer: S) -> std::result::Result + where + S: Serializer, + { + serializer.serialize_str(self.to_string().as_ref()) + } +} + +impl<'de> Deserialize<'de> for ScheduleEvery { + fn deserialize(deserializer: D) -> std::result::Result + where + D: Deserializer<'de>, + { + let s = String::deserialize(deserializer)?; + match s.to_lowercase().as_str() { + "year" => Ok(Self::Year), + "month" => Ok(Self::Month), + "twoweeks" => Ok(Self::TwoWeeks), + "week" => Ok(Self::Week), + "day" => Ok(Self::Day), + "hour" => Ok(Self::Hour), + "minute" => Ok(Self::Minute), + "second" => Ok(Self::Second), + _ => Err(DeError::custom(format!("unknown every kind '{s}'"))), + } + } +} + +/// Defines when a notification is delivered. +/// +/// Scheduling is only implemented on mobile; the desktop implementation delivers the +/// notification immediately and ignores the schedule. +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub enum Schedule { + /// Fires at a specific date and time, which must be in the future. + #[serde(rename_all = "camelCase")] + At { + /// The date and time the notification fires at, serialized as an ISO-8601 string. + #[serde( + serialize_with = "iso8601::serialize", + deserialize_with = "time::serde::iso8601::deserialize" + )] + date: time::OffsetDateTime, + /// Whether the notification keeps repeating, using the duration between the moment it is + /// scheduled and `date` as the interval. Defaults to `false`. + /// + /// The interval must be at least one minute on iOS. + #[serde(default)] + repeating: bool, + /// Whether the notification is allowed to fire while the device is in low-power idle + /// (Doze) mode. Defaults to `false`. + /// + /// Only used on Android. + #[serde(default)] + allow_while_idle: bool, + }, + /// Fires whenever the current date matches every field set on the given interval. + #[serde(rename_all = "camelCase")] + Interval { + /// The date fields the current date must match for the notification to fire. + interval: ScheduleInterval, + /// Whether the notification is allowed to fire while the device is in low-power idle + /// (Doze) mode. Defaults to `false`. + /// + /// Only used on Android. + #[serde(default)] + allow_while_idle: bool, + }, + /// Fires repeatedly, once every `count` times the given interval unit. + #[serde(rename_all = "camelCase")] + Every { + /// The unit of the repeating interval. + interval: ScheduleEvery, + /// How many interval units elapse between each notification. + count: u8, + /// Whether the notification is allowed to fire while the device is in low-power idle + /// (Doze) mode. Defaults to `false`. + /// + /// Only used on Android. + #[serde(default)] + allow_while_idle: bool, + }, +} + +// custom ISO-8601 serialization that does not use 6 digits for years. +mod iso8601 { + use serde::{ser::Error as _, Serialize, Serializer}; + use time::{ + format_description::well_known::iso8601::{Config, EncodedConfig}, + format_description::well_known::Iso8601, + OffsetDateTime, + }; + + const SERDE_CONFIG: EncodedConfig = Config::DEFAULT.encode(); + + pub fn serialize( + datetime: &OffsetDateTime, + serializer: S, + ) -> Result { + datetime + .format(&Iso8601::) + .map_err(S::Error::custom)? + .serialize(serializer) + } +} + +/// The payload of a notification, as sent to the platform implementation. +/// +/// Build it with [`NotificationBuilder`](crate::NotificationBuilder) rather than constructing it directly. +/// The identifier defaults to a random 32-bit integer when it is not provided. +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct NotificationData { + #[serde(default = "default_id")] + pub(crate) id: i32, + pub(crate) channel_id: Option, + pub(crate) title: Option, + pub(crate) body: Option, + pub(crate) schedule: Option, + pub(crate) large_body: Option, + pub(crate) summary: Option, + pub(crate) action_type_id: Option, + pub(crate) group: Option, + #[serde(default)] + pub(crate) group_summary: bool, + pub(crate) sound: Option, + #[serde(default)] + pub(crate) inbox_lines: Vec, + pub(crate) icon: Option, + pub(crate) large_icon: Option, + pub(crate) icon_color: Option, + #[serde(default)] + pub(crate) attachments: Vec, + #[serde(default)] + pub(crate) extra: HashMap, + #[serde(default)] + pub(crate) ongoing: bool, + #[serde(default)] + pub(crate) auto_cancel: bool, + #[serde(default)] + pub(crate) silent: bool, +} + +fn default_id() -> i32 { + rand::random() +} + +impl Default for NotificationData { + fn default() -> Self { + Self { + id: default_id(), + channel_id: None, + title: None, + body: None, + schedule: None, + large_body: None, + summary: None, + action_type_id: None, + group: None, + group_summary: false, + sound: None, + inbox_lines: Vec::new(), + icon: None, + large_icon: None, + icon_color: None, + attachments: Vec::new(), + extra: Default::default(), + ongoing: false, + auto_cancel: false, + silent: false, + } + } +} + +/// A notification that was scheduled and has not been delivered yet. +/// +/// Returned by `Notification::pending`, which is only available on mobile. +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct PendingNotification { + id: i32, + title: Option, + body: Option, + schedule: Schedule, +} + +impl PendingNotification { + /// The notification identifier. + pub fn id(&self) -> i32 { + self.id + } + + /// The notification title, if it was set. + pub fn title(&self) -> Option<&str> { + self.title.as_deref() + } + + /// The notification body, if it was set. + pub fn body(&self) -> Option<&str> { + self.body.as_deref() + } + + /// The schedule that determines when the notification is delivered. + pub fn schedule(&self) -> &Schedule { + &self.schedule + } +} + +/// A notification that was delivered and is still visible in the notification center. +/// +/// Returned by `Notification::active`, which is only available on mobile. +/// Which fields are populated depends on the platform, since Android and iOS expose +/// different information about delivered notifications. +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ActiveNotification { + id: i32, + tag: Option, + title: Option, + body: Option, + group: Option, + #[serde(default)] + group_summary: bool, + #[serde(default)] + data: HashMap, + #[serde(default)] + extra: HashMap, + #[serde(default)] + attachments: Vec, + action_type_id: Option, + schedule: Option, + sound: Option, +} + +impl ActiveNotification { + /// The notification identifier. + pub fn id(&self) -> i32 { + self.id + } + + /// The tag the notification was posted with. + /// + /// Only set on Android. + pub fn tag(&self) -> Option<&str> { + self.tag.as_deref() + } + + /// The notification title, if it was set. + pub fn title(&self) -> Option<&str> { + self.title.as_deref() + } + + /// The notification body, if it was set. + pub fn body(&self) -> Option<&str> { + self.body.as_deref() + } + + /// The identifier of the group the notification belongs to. + /// + /// Only set on Android. + pub fn group(&self) -> Option<&str> { + self.group.as_deref() + } + + /// Whether the notification is the summary of its group. + /// + /// Only set on Android. Defaults to `false`. + pub fn group_summary(&self) -> bool { + self.group_summary + } + + /// The platform extras attached to the notification, as string values. + /// + /// Only set on Android, where it holds the `android.app.Notification` extras bundle. + pub fn data(&self) -> &HashMap { + &self.data + } + + /// The extra payload that was stored in the notification. + pub fn extra(&self) -> &HashMap { + &self.extra + } + + /// The attachments of the notification. + /// + /// Only set on iOS. + pub fn attachments(&self) -> &[Attachment] { + &self.attachments + } + + /// The identifier of the action type the notification was registered with. + /// + /// Only set on iOS. + pub fn action_type_id(&self) -> Option<&str> { + self.action_type_id.as_deref() + } + + /// The schedule the notification was delivered with, if it was scheduled. + pub fn schedule(&self) -> Option<&Schedule> { + self.schedule.as_ref() + } + + /// The sound resource name of the notification. + /// + /// Only set on iOS. + pub fn sound(&self) -> Option<&str> { + self.sound.as_deref() + } +} + +/// A group of [`Action`]s a notification can display, referenced by +/// [`NotificationBuilder::action_type_id`](crate::NotificationBuilder::action_type_id). +/// +/// Register it with `Notification::register_action_types` before sending a notification that uses it. +/// It maps to a `UNNotificationCategory` on iOS and to an action group on Android. +/// +/// Only available on mobile. Use [`ActionType::builder`] to construct one. +#[cfg(mobile)] +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct ActionType { + id: String, + actions: Vec, + hidden_previews_body_placeholder: Option, + custom_dismiss_action: bool, + allow_in_car_play: bool, + hidden_previews_show_title: bool, + hidden_previews_show_subtitle: bool, +} + +/// Builder for an [`ActionType`], created with [`ActionType::builder`]. +/// +/// Only available on mobile. +#[cfg(mobile)] +#[derive(Debug)] +pub struct ActionTypeBuilder(ActionType); + +#[cfg(mobile)] +impl ActionType { + /// Creates a builder for an action type with the given identifier. + /// + /// All the optional settings default to `false` or [`None`]; + /// call [`ActionTypeBuilder::build`] to get the [`ActionType`]. + pub fn builder(id: impl Into) -> ActionTypeBuilder { + ActionTypeBuilder(Self { + id: id.into(), + actions: Vec::new(), + hidden_previews_body_placeholder: None, + custom_dismiss_action: false, + allow_in_car_play: false, + hidden_previews_show_title: false, + hidden_previews_show_subtitle: false, + }) + } + + /// The identifier of this action type. + pub fn id(&self) -> &str { + &self.id + } + + /// The actions associated with this action type. + pub fn actions(&self) -> &[Action] { + &self.actions + } + + /// The placeholder shown instead of the notification body when previews are hidden. + /// + /// Only used on iOS. + pub fn hidden_previews_body_placeholder(&self) -> Option<&str> { + self.hidden_previews_body_placeholder.as_deref() + } + + /// Whether the app is notified when the user dismisses the notification. + /// + /// Only used on iOS. + pub fn custom_dismiss_action(&self) -> bool { + self.custom_dismiss_action + } + + /// Whether the notification can be displayed in a CarPlay environment. + /// + /// Only used on iOS. + pub fn allow_in_car_play(&self) -> bool { + self.allow_in_car_play + } + + /// Whether the notification title is shown even when previews are hidden. + /// + /// Only used on iOS. + pub fn hidden_previews_show_title(&self) -> bool { + self.hidden_previews_show_title + } + + /// Whether the notification subtitle is shown even when previews are hidden. + /// + /// Only used on iOS. + pub fn hidden_previews_show_subtitle(&self) -> bool { + self.hidden_previews_show_subtitle + } +} + +#[cfg(mobile)] +impl ActionTypeBuilder { + /// Sets the actions associated with this action type. + pub fn actions(mut self, actions: Vec) -> Self { + self.0.actions = actions; + self + } + + /// Sets the placeholder shown instead of the notification body when previews are hidden. + /// + /// Only used on iOS. + pub fn hidden_previews_body_placeholder( + mut self, + hidden_previews_body_placeholder: impl Into, + ) -> Self { + self.0 + .hidden_previews_body_placeholder + .replace(hidden_previews_body_placeholder.into()); + self + } + + /// Sets whether the app is notified when the user dismisses the notification. + /// + /// Only used on iOS. + pub fn custom_dismiss_action(mut self, custom_dismiss_action: bool) -> Self { + self.0.custom_dismiss_action = custom_dismiss_action; + self + } + + /// Sets whether the notification can be displayed in a CarPlay environment. + /// + /// Only used on iOS. + pub fn allow_in_car_play(mut self, allow_in_car_play: bool) -> Self { + self.0.allow_in_car_play = allow_in_car_play; + self + } + + /// Sets whether the notification title is shown even when previews are hidden. + /// + /// Only used on iOS. + pub fn hidden_previews_show_title(mut self, hidden_previews_show_title: bool) -> Self { + self.0.hidden_previews_show_title = hidden_previews_show_title; + self + } + + /// Sets whether the notification subtitle is shown even when previews are hidden. + /// + /// Only used on iOS. + pub fn hidden_previews_show_subtitle(mut self, hidden_previews_show_subtitle: bool) -> Self { + self.0.hidden_previews_show_subtitle = hidden_previews_show_subtitle; + self + } + + /// Builds the [`ActionType`]. + pub fn build(self) -> ActionType { + self.0 + } +} + +/// A button the user can tap on a notification, belonging to an [`ActionType`]. +/// +/// It maps to a `UNNotificationAction` on iOS. On Android only the identifier, the title +/// and the input flag are used. +/// +/// Only available on mobile. Use [`Action::builder`] to construct one. +#[cfg(mobile)] +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct Action { + id: String, + title: String, + requires_authentication: bool, + foreground: bool, + destructive: bool, + input: bool, + input_button_title: Option, + input_placeholder: Option, +} + +/// Builder for an [`Action`], created with [`Action::builder`]. +/// +/// Only available on mobile. +#[cfg(mobile)] +#[derive(Debug)] +pub struct ActionBuilder(Action); + +#[cfg(mobile)] +impl Action { + /// Creates a builder for an action with the given identifier and button title. + /// + /// All the optional settings default to `false` or [`None`]; + /// call [`ActionBuilder::build`] to get the [`Action`]. + pub fn builder(id: impl Into, title: impl Into) -> ActionBuilder { + ActionBuilder(Self { + id: id.into(), + title: title.into(), + requires_authentication: false, + foreground: false, + destructive: false, + input: false, + input_button_title: None, + input_placeholder: None, + }) + } + + /// The identifier of this action, reported back when the user triggers it. + pub fn id(&self) -> &str { + &self.id + } + + /// The text displayed on the action button. + pub fn title(&self) -> &str { + &self.title + } + + /// Whether the device must be unlocked for the action to run. + /// + /// Only used on iOS. + pub fn requires_authentication(&self) -> bool { + self.requires_authentication + } + + /// Whether the app is brought to the foreground when the action is triggered. + /// + /// Only used on iOS. + pub fn foreground(&self) -> bool { + self.foreground + } + + /// Whether the action is displayed as destructive, usually in red. + /// + /// Only used on iOS. + pub fn destructive(&self) -> bool { + self.destructive + } + + /// Whether triggering the action lets the user type a text response. + pub fn input(&self) -> bool { + self.input + } + + /// The text displayed on the button that submits the text input. + /// + /// Only used on iOS. + pub fn input_button_title(&self) -> Option<&str> { + self.input_button_title.as_deref() + } + + /// The placeholder displayed on the empty text input field. + /// + /// Only used on iOS. + pub fn input_placeholder(&self) -> Option<&str> { + self.input_placeholder.as_deref() + } +} + +#[cfg(mobile)] +impl ActionBuilder { + /// Sets whether the device must be unlocked for the action to run. + /// + /// Only used on iOS. + pub fn requires_authentication(mut self, requires_authentication: bool) -> Self { + self.0.requires_authentication = requires_authentication; + self + } + + /// Sets whether the app is brought to the foreground when the action is triggered. + /// + /// Only used on iOS. + pub fn foreground(mut self, foreground: bool) -> Self { + self.0.foreground = foreground; + self + } + + /// Sets whether the action is displayed as destructive, usually in red. + /// + /// Only used on iOS. + pub fn destructive(mut self, destructive: bool) -> Self { + self.0.destructive = destructive; + self + } + + /// Sets whether triggering the action lets the user type a text response. + pub fn input(mut self, input: bool) -> Self { + self.0.input = input; + self + } + + /// Sets the text displayed on the button that submits the text input. + /// + /// Only used on iOS. + pub fn input_button_title(mut self, input_button_title: impl Into) -> Self { + self.0.input_button_title.replace(input_button_title.into()); + self + } + + /// Sets the placeholder displayed on the empty text input field. + /// + /// Only used on iOS. + pub fn input_placeholder(mut self, input_placeholder: impl Into) -> Self { + self.0.input_placeholder.replace(input_placeholder.into()); + self + } + + /// Builds the [`Action`]. + pub fn build(self) -> Action { + self.0 + } +} + +#[cfg(target_os = "android")] +pub use android::*; + +#[cfg(target_os = "android")] +mod android { + use serde::{Deserialize, Serialize}; + use serde_repr::{Deserialize_repr, Serialize_repr}; + + /// How much the notifications of a [`Channel`] interrupt the user. + /// + /// It maps to the `NotificationManager.IMPORTANCE_*` constants and is serialized as its + /// integer value. Only available on Android. + #[derive(Debug, Clone, Copy, Serialize_repr, Deserialize_repr)] + #[repr(u8)] + pub enum Importance { + /// The notifications are not shown. + None = 0, + /// The notifications are only shown in the shade, below the fold, without a status bar icon. + Min = 1, + /// The notifications are shown without a sound. + Low = 2, + /// The notifications are shown and make a sound. + /// + /// This is the value used when the channel does not define an importance. + Default = 3, + /// The notifications are shown, make a sound and pop up as a heads-up notification. + High = 4, + } + + impl Default for Importance { + fn default() -> Self { + Self::Default + } + } + + /// How much of a notification is shown on the lock screen. + /// + /// It maps to the `Notification.VISIBILITY_*` constants and is serialized as its + /// integer value. Only available on Android. + #[derive(Debug, Clone, Copy, Serialize_repr, Deserialize_repr)] + #[repr(i8)] + pub enum Visibility { + /// The notification is not shown on the lock screen at all. + Secret = -1, + /// The notification is shown on the lock screen with its sensitive content hidden. + /// + /// This is the value used when the channel does not define a visibility. + Private = 0, + /// The notification is shown in full on the lock screen. + Public = 1, + } + + /// A notification channel, the category users configure notification behavior on. + /// + /// Notifications reference a channel through + /// [`NotificationBuilder::channel_id`](crate::NotificationBuilder::channel_id) and are not + /// delivered when the channel does not exist. Only available on Android. + /// Use [`Channel::builder`] to construct one. + #[derive(Debug, Serialize, Deserialize)] + #[serde(rename_all = "camelCase")] + pub struct Channel { + id: String, + name: String, + description: Option, + sound: Option, + lights: bool, + light_color: Option, + vibration: bool, + importance: Importance, + visibility: Option, + } + + /// Builder for a [`Channel`], created with [`Channel::builder`]. + /// + /// Only available on Android. + #[derive(Debug)] + pub struct ChannelBuilder(Channel); + + impl Channel { + /// Creates a builder for a channel with the given identifier and user visible name. + /// + /// Lights and vibration are disabled, the importance defaults to [`Importance::Default`] + /// and the remaining settings default to [`None`]; + /// call [`ChannelBuilder::build`] to get the [`Channel`]. + pub fn builder(id: impl Into, name: impl Into) -> ChannelBuilder { + ChannelBuilder(Self { + id: id.into(), + name: name.into(), + description: None, + sound: None, + lights: false, + light_color: None, + vibration: false, + importance: Default::default(), + visibility: None, + }) + } + + /// The identifier of this channel. + pub fn id(&self) -> &str { + &self.id + } + + /// The user visible name of this channel. + pub fn name(&self) -> &str { + &self.name + } + + /// The user visible description of this channel. + pub fn description(&self) -> Option<&str> { + self.description.as_deref() + } + + /// The name of the sound resource played by the notifications of this channel. + /// + /// The resource must be placed in the app's `res/raw` folder. + pub fn sound(&self) -> Option<&str> { + self.sound.as_deref() + } + + /// Whether the notifications of this channel blink the device light. + pub fn lights(&self) -> bool { + self.lights + } + + /// The color of the device light, as a color string such as `#ff0000`. + pub fn light_color(&self) -> Option<&str> { + self.light_color.as_deref() + } + + /// Whether the notifications of this channel vibrate the device. + pub fn vibration(&self) -> bool { + self.vibration + } + + /// How much the notifications of this channel interrupt the user. + pub fn importance(&self) -> Importance { + self.importance + } + + /// How much of the notifications of this channel is shown on the lock screen. + /// + /// [`Visibility::Private`] is used when this is [`None`]. + pub fn visibility(&self) -> Option { + self.visibility + } + } + + impl ChannelBuilder { + /// Sets the user visible description of the channel. + pub fn description(mut self, description: impl Into) -> Self { + self.0.description.replace(description.into()); + self + } + + /// Sets the name of the sound resource played by the notifications of this channel. + /// + /// The resource must be placed in the app's `res/raw` folder. + pub fn sound(mut self, sound: impl Into) -> Self { + self.0.sound.replace(sound.into()); + self + } + + /// Sets whether the notifications of this channel blink the device light. + pub fn lights(mut self, lights: bool) -> Self { + self.0.lights = lights; + self + } + + /// Sets the color of the device light, as a color string such as `#ff0000`. + pub fn light_color(mut self, color: impl Into) -> Self { + self.0.light_color.replace(color.into()); + self + } + + /// Sets whether the notifications of this channel vibrate the device. + pub fn vibration(mut self, vibration: bool) -> Self { + self.0.vibration = vibration; + self + } + + /// Sets how much the notifications of this channel interrupt the user. + pub fn importance(mut self, importance: Importance) -> Self { + self.0.importance = importance; + self + } + + /// Sets how much of the notifications of this channel is shown on the lock screen. + pub fn visibility(mut self, visibility: Visibility) -> Self { + self.0.visibility.replace(visibility); + self + } + + /// Builds the [`Channel`]. + pub fn build(self) -> Channel { + self.0 + } + } +} diff --git a/plugins/notification/test/tauri.conf.json b/plugins/notification/test/tauri.conf.json new file mode 100644 index 0000000..4d0ae02 --- /dev/null +++ b/plugins/notification/test/tauri.conf.json @@ -0,0 +1,21 @@ +{ + "identifier": "app.tauri.example", + "build": { + "frontendDist": ".", + "devUrl": "http://localhost:4000" + }, + "app": { + "windows": [ + { + "title": "Tauri App" + } + ], + "security": { + "csp": "default-src blob: data: filesystem: ws: wss: http: https: tauri: http://tauri.localhost 'unsafe-eval' 'unsafe-inline' 'self'" + } + }, + "bundle": { + "active": true, + "icon": ["../../../examples/api/src-tauri/icons/icon.png"] + } +} diff --git a/plugins/notification/tsconfig.json b/plugins/notification/tsconfig.json new file mode 100644 index 0000000..4374468 --- /dev/null +++ b/plugins/notification/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "rootDir": "guest-js" + }, + "include": ["guest-js/*.ts"] +} diff --git a/plugins/opener/CHANGELOG.md b/plugins/opener/CHANGELOG.md new file mode 100644 index 0000000..e11e047 --- /dev/null +++ b/plugins/opener/CHANGELOG.md @@ -0,0 +1,80 @@ +# Changelog + +## [2.5.5] + +- [`db9c5998`](https://github.com/tauri-apps/plugins-workspace/commit/db9c5998feff9384f9cbbefcbe0d45937c00a1fc) ([#3531](https://github.com/tauri-apps/plugins-workspace/pull/3531) by [@Legend-Master](https://github.com/tauri-apps/plugins-workspace/../../Legend-Master)) Fix missing `consumer-rules.pro` on Gradle v9. + +## \[2.5.4] + +- [`c1fd33b3`](https://github.com/tauri-apps/plugins-workspace/commit/c1fd33b3a2735f2e25c1d026dc524af932db3315) ([#3343](https://github.com/tauri-apps/plugins-workspace/pull/3343) by [@Legend-Master](https://github.com/tauri-apps/plugins-workspace/../../Legend-Master)) Fix `revealItemInDir`/`reveal_items_in_dir` can't reveal network paths like `\\wsl.localhost\Ubuntu\etc` on Windows + +## \[2.5.3] + +- [`3d0d2e04`](https://github.com/tauri-apps/plugins-workspace/commit/3d0d2e041bbad9766aebecaeba291a28d8d7bf5c) ([#3163](https://github.com/tauri-apps/plugins-workspace/pull/3163) by [@FabianLars](https://github.com/tauri-apps/plugins-workspace/../../FabianLars)) Properly ignore `with: inAppBrowser` on desktop. This prevents an issue were `open_url` seamingly did nothing on desktop. + +## \[2.5.2] + +- [`93426f85`](https://github.com/tauri-apps/plugins-workspace/commit/93426f85120f49beb9f40222bff45185a32d54a9) Fixed an issue that caused docs.rs builds to fail. No user facing changes. + +## \[2.5.1] + +- [`6c9b61fb`](https://github.com/tauri-apps/plugins-workspace/commit/6c9b61fb658145d13893626112fc489f7458aa17) ([#3039](https://github.com/tauri-apps/plugins-workspace/pull/3039) by [@FabianLars](https://github.com/tauri-apps/plugins-workspace/../../FabianLars)) On Android, updated compileSdk to 36. +- [`67a7bf80`](https://github.com/tauri-apps/plugins-workspace/commit/67a7bf80f8e6f3aec76ebaba0983f0cf0103d3ff) ([#3018](https://github.com/tauri-apps/plugins-workspace/pull/3018) by [@Legend-Master](https://github.com/tauri-apps/plugins-workspace/../../Legend-Master)) Fix opener doesn't open same origin links in the browser + +## \[2.5.0] + +### enhance + +- [`b8056f48`](https://github.com/tauri-apps/plugins-workspace/commit/b8056f484c7144af095d4d6ded1e8adbb9b8a865) ([#2897](https://github.com/tauri-apps/plugins-workspace/pull/2897) by [@petersamokhin](https://github.com/tauri-apps/plugins-workspace/../../petersamokhin)) Allow reveal multiple items in the file explorer. + +## \[2.4.0] + +- [`f209b2f2`](https://github.com/tauri-apps/plugins-workspace/commit/f209b2f23cb29133c97ad5961fb46ef794dbe063) ([#2804](https://github.com/tauri-apps/plugins-workspace/pull/2804) by [@renovate](https://github.com/tauri-apps/plugins-workspace/../../renovate)) Updated tauri to 2.6 + +## \[2.3.1] + +### feat + +- [`2aec8ff4`](https://github.com/tauri-apps/plugins-workspace/commit/2aec8ff4c41d178ea9804f7b6eff343c726be015) ([#2803](https://github.com/tauri-apps/plugins-workspace/pull/2803) by [@lucasfernog](https://github.com/tauri-apps/plugins-workspace/../../lucasfernog)) Add `inAppBrowser` option to open URLs in an in-app browser on Android and iOS. + +## \[2.3.0] + +- [`ce9888a2`](https://github.com/tauri-apps/plugins-workspace/commit/ce9888a2d4c9b449bd2a306f0fa6c76507fd46d3) ([#2762](https://github.com/tauri-apps/plugins-workspace/pull/2762)) Similar to the `fs` plugin the `opener` plugin now supports a `requireLiteralLeadingDot` configuration in `tauri.conf.json`. + +## \[2.2.7] + +- [`6c9e08dc`](https://github.com/tauri-apps/plugins-workspace/commit/6c9e08dccb3ac99fccfce586fa2b69717ba81b52) ([#2695](https://github.com/tauri-apps/plugins-workspace/pull/2695) by [@ShaunSHamilton](https://github.com/tauri-apps/plugins-workspace/../../ShaunSHamilton)) Adjust `open_url` url type to allow `URL` +- [`dde6f3c3`](https://github.com/tauri-apps/plugins-workspace/commit/dde6f3c31c1b79942abb556be31757dc583f509e) ([#2549](https://github.com/tauri-apps/plugins-workspace/pull/2549) by [@renovate](https://github.com/tauri-apps/plugins-workspace/../../renovate)) Update `windows` crate to 0.61 to align with Tauri 2.5 + +## \[2.2.6] + +- [`1a984659`](https://github.com/tauri-apps/plugins-workspace/commit/1a9846599b6a71faf330845847a30f6bf9735898) ([#2469](https://github.com/tauri-apps/plugins-workspace/pull/2469) by [@FabianLars](https://github.com/tauri-apps/plugins-workspace/../../FabianLars)) Update `objc2` crate to 0.6. No user facing changes. +- [`71f95c9f`](https://github.com/tauri-apps/plugins-workspace/commit/71f95c9f05b29cf1be586849614c0b007757c15d) ([#2445](https://github.com/tauri-apps/plugins-workspace/pull/2445) by [@renovate](https://github.com/tauri-apps/plugins-workspace/../../renovate)) Updated `windows` crate to 0.60 to match Tauri 2.3.0. No user facing changes. + +## \[2.2.5] + +- [`5b821181`](https://github.com/tauri-apps/plugins-workspace/commit/5b8211815825ddae2dcc0c00520e0cfdff002763) ([#2332](https://github.com/tauri-apps/plugins-workspace/pull/2332) by [@betamos](https://github.com/tauri-apps/plugins-workspace/../../betamos)) Fix broken JS commands `opener.openPath` and `opener.openUrl` on mobile. + +## \[2.2.4] + +- [`da5c59e2`](https://github.com/tauri-apps/plugins-workspace/commit/da5c59e2fe879d177e3cfd52fcacce85440423cb) ([#2271](https://github.com/tauri-apps/plugins-workspace/pull/2271) by [@renovate](https://github.com/tauri-apps/plugins-workspace/../../renovate)) Updated `zbus` dependency to version 5. No API changes. + +## \[2.2.3] + +- [`a9ac1e3c`](https://github.com/tauri-apps/plugins-workspace/commit/a9ac1e3c939cec4338a9422ef02323c1d4dde6cd) ([#2253](https://github.com/tauri-apps/plugins-workspace/pull/2253) by [@betamos](https://github.com/tauri-apps/plugins-workspace/../../betamos)) Return an error in `open_path` if the file does not exist when opening with default application. + +## \[2.2.2] + +- [`ee0f65de`](https://github.com/tauri-apps/plugins-workspace/commit/ee0f65de5c645c244c5f0b638e0e0aab687cb9bf) ([#2207](https://github.com/tauri-apps/plugins-workspace/pull/2207) by [@universalappfactory](https://github.com/tauri-apps/plugins-workspace/../../universalappfactory)) Fixed OpenerPlugin packagename for android + +## \[2.2.1] + +- [`18dffc9d`](https://github.com/tauri-apps/plugins-workspace/commit/18dffc9dfecaf0c900e233e041d9ca36c92834b5) ([#2189](https://github.com/tauri-apps/plugins-workspace/pull/2189) by [@lucasfernog](https://github.com/tauri-apps/plugins-workspace/../../lucasfernog)) Fix usage on iOS. + +## \[2.2.0] + +- [`3a79266b`](https://github.com/tauri-apps/plugins-workspace/commit/3a79266b8cf96a55b1ae6339d725567d45a44b1d) ([#2173](https://github.com/tauri-apps/plugins-workspace/pull/2173) by [@FabianLars](https://github.com/tauri-apps/plugins-workspace/../../FabianLars)) Bumped all plugins to `v2.2.0`. From now, the versions for the Rust and JavaScript packages of each plugin will be in sync with each other. + +## \[2.0.0] + +- [`383e636a`](https://github.com/tauri-apps/plugins-workspace/commit/383e636a8e595aec1300999a8aeb7d9bf8c14632) ([#2019](https://github.com/tauri-apps/plugins-workspace/pull/2019) by [@amrbashir](https://github.com/tauri-apps/plugins-workspace/../../amrbashir)) Initial Release diff --git a/plugins/opener/Cargo.toml b/plugins/opener/Cargo.toml new file mode 100644 index 0000000..e17b530 --- /dev/null +++ b/plugins/opener/Cargo.toml @@ -0,0 +1,67 @@ +[package] +name = "tauri-plugin-opener" +version = "2.5.5" +description = "Open files and URLs using their default application." +edition = { workspace = true } +authors = { workspace = true } +license = { workspace = true } +repository = { workspace = true } +links = "tauri-plugin-opener" + +# Platforms supported by the plugin +# Support levels are "full", "partial", "none", "unknown" +# Details of the support level are left to plugin maintainer +exclude = ["/guest-js", "/package.json", "/rollup.config.js", "/tsconfig.json"] + +[package.metadata.platforms.support] +windows = { level = "full", notes = "" } +linux = { level = "full", notes = "" } +macos = { level = "full", notes = "" } +android = { level = "partial", notes = "Only allows to open URLs via `open`" } +ios = { level = "partial", notes = "Only allows to open URLs via `open`" } + + +[build-dependencies] +tauri-plugin = { workspace = true, features = ["build"] } +schemars = { workspace = true } +serde = { workspace = true } + +[dev-dependencies] +tauri = { workspace = true, features = ["wry"] } + +[dependencies] +serde = { workspace = true } +serde_json = { workspace = true } +tauri = { workspace = true } +thiserror = { workspace = true } +open = { version = "5", features = ["shellexecute-on-windows"] } +glob = { workspace = true } +dunce = { workspace = true } + +[target."cfg(windows)".dependencies.windows] +version = "0.61" +features = [ + "Win32_Foundation", + "Win32_UI_Shell_Common", + "Win32_UI_WindowsAndMessaging", + "Win32_System_Com", + "Win32_System_Registry", + "Win32_Storage_FileSystem", +] + +[target."cfg(any(target_os = \"linux\", target_os = \"dragonfly\", target_os = \"freebsd\", target_os = \"netbsd\", target_os = \"openbsd\"))".dependencies] +zbus = { workspace = true } +url = { workspace = true } + +[target."cfg(target_os = \"macos\")".dependencies.objc2-app-kit] +version = "0.3" +default-features = false +features = ["std", "NSWorkspace"] + +[target."cfg(target_os = \"macos\")".dependencies.objc2-foundation] +version = "0.3" +default-features = false +features = ["std", "NSURL", "NSArray", "NSString"] + +[target.'cfg(target_os = "ios")'.dependencies] +tauri = { workspace = true, features = ["wry"] } diff --git a/plugins/opener/LICENSE.spdx b/plugins/opener/LICENSE.spdx new file mode 100644 index 0000000..cdd0df5 --- /dev/null +++ b/plugins/opener/LICENSE.spdx @@ -0,0 +1,20 @@ +SPDXVersion: SPDX-2.1 +DataLicense: CC0-1.0 +PackageName: tauri +DataFormat: SPDXRef-1 +PackageSupplier: Organization: The Tauri Programme in the Commons Conservancy +PackageHomePage: https://tauri.app +PackageLicenseDeclared: Apache-2.0 +PackageLicenseDeclared: MIT +PackageCopyrightText: 2019-2022, The Tauri Programme in the Commons Conservancy +PackageSummary: Tauri is a rust project that enables developers to make secure +and small desktop applications using a web frontend. + +PackageComment: The package includes the following libraries; see +Relationship information. + +Created: 2019-05-20T09:00:00Z +PackageDownloadLocation: git://github.com/tauri-apps/tauri +PackageDownloadLocation: git+https://github.com/tauri-apps/tauri.git +PackageDownloadLocation: git+ssh://github.com/tauri-apps/tauri.git +Creator: Person: Daniel Thompson-Yvetot \ No newline at end of file diff --git a/plugins/opener/LICENSE_APACHE-2.0 b/plugins/opener/LICENSE_APACHE-2.0 new file mode 100644 index 0000000..4947287 --- /dev/null +++ b/plugins/opener/LICENSE_APACHE-2.0 @@ -0,0 +1,177 @@ + + 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 \ No newline at end of file diff --git a/plugins/opener/LICENSE_MIT b/plugins/opener/LICENSE_MIT new file mode 100644 index 0000000..4d75472 --- /dev/null +++ b/plugins/opener/LICENSE_MIT @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2017 - Present Tauri Apps 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. \ No newline at end of file diff --git a/plugins/opener/README.md b/plugins/opener/README.md new file mode 100644 index 0000000..bf25c5b --- /dev/null +++ b/plugins/opener/README.md @@ -0,0 +1,143 @@ +![opener](https://github.com/tauri-apps/plugins-workspace/raw/v2/plugins/opener/banner.png) + + + +| Platform | Supported | +| -------- | --------- | +| Linux | ✓ | +| Windows | ✓ | +| macOS | ✓ | +| Android | ✓ | +| iOS | ✓ | + +## Install + +_This plugin requires a Rust version of at least **1.77.2**_ + +There are three general methods of installation that we can recommend. + +1. Use crates.io and npm (easiest, and requires you to trust that our publishing pipeline worked) +2. Pull sources directly from Github using git tags / revision hashes (most secure) +3. Git submodule install this repo in your tauri project and then use file protocol to ingest the source (most secure, but inconvenient to use) + +Install the Core plugin by adding the following to your `Cargo.toml` file: + +`src-tauri/Cargo.toml` + +```toml +[dependencies] +tauri-plugin-opener = "2.0.0" +# alternatively with Git: +tauri-plugin-opener = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v2" } +``` + +You can install the JavaScript Guest bindings using your preferred JavaScript package manager: + + + +```sh +pnpm add @tauri-apps/plugin-opener +# or +npm add @tauri-apps/plugin-opener +# or +yarn add @tauri-apps/plugin-opener +``` + +## Usage + +First you need to register the core plugin with Tauri: + +`src-tauri/src/lib.rs` + +```rust +fn main() { + tauri::Builder::default() + .plugin(tauri_plugin_opener::init()) + .run(tauri::generate_context!()) + .expect("error while running tauri application"); +} +``` + +Afterwards all the plugin's APIs are available through the JavaScript guest bindings: + +```javascript +import { openUrl, openPath, revealItemInDir } from '@tauri-apps/plugin-opener' + +// Opens the URL in the default browser +await openUrl('https://example.com') +// Or with a specific browser/app +await openUrl('https://example.com', 'firefox') + +// Opens the path with the system's default app +await openPath('/path/to/file') +// Or with a specific app +await openPath('/path/to/file', 'firefox') + +// Reveal a path with the system's default explorer +await revealItemInDir('/path/to/file') + +// Reveal multiple paths with the system's default explorer +// Note: will be renamed to `revealItemsInDir` in the next major version +await revealItemInDir(['/path/to/file', '/path/to/another/file']) +``` + +### Usage from Rust + +You can also use those APIs from Rust: + +```rust +use tauri_plugin_opener::OpenerExt; + +fn main() { + tauri::Builder::default() + .plugin(tauri_plugin_opener::init()) + .setup(|app| { + let opener = app.opener(); + + // Opens the URL in the default browser + opener.open_url("https://example.com", None::<&str>)?; + // Or with a specific browser/app + opener.open_url("https://example.com", Some("firefox"))?; + + // Opens the path with the system's default app + opener.open_path("/path/to/file", None::<&str>)?; + // Or with a specific app + opener.open_path("/path/to/file", Some("firefox"))?; + + // Reveal a path with the system's default explorer + opener.reveal_item_in_dir("/path/to/file")?; + + // Reveal multiple paths with the system's default explorer + opener.reveal_items_in_dir(["/path/to/file"])?; + Ok(()) + }) + .run(tauri::generate_context!()) + .expect("error while running tauri application"); +} +``` + +## Contributing + +PRs accepted. Please make sure to read the Contributing Guide before making a pull request. + +## Partners + + + + + + + +
+ + CrabNebula + +
+ +For the complete list of sponsors please visit our [website](https://tauri.app#sponsors) and [Open Collective](https://opencollective.com/tauri). + +## License + +Code: (c) 2015 - Present - The Tauri Programme within The Commons Conservancy. + +MIT or MIT/Apache 2.0 where applicable. diff --git a/plugins/opener/SECURITY.md b/plugins/opener/SECURITY.md new file mode 100644 index 0000000..4f09bba --- /dev/null +++ b/plugins/opener/SECURITY.md @@ -0,0 +1,23 @@ +# Security Policy + +**Do not report security vulnerabilities through public GitHub issues.** + +**Please use the [Private Vulnerability Disclosure](https://docs.github.com/en/code-security/security-advisories/guidance-on-reporting-and-writing-information-about-vulnerabilities/privately-reporting-a-security-vulnerability#privately-reporting-a-security-vulnerability) feature of GitHub.** + +Include as much of the following information: + +- Type of issue (e.g. improper input parsing, privilege escalation, etc.) +- The location of the affected source code (tag/branch/commit or direct URL) +- Any special configuration required to reproduce the issue +- The distribution affected or used to help us with reproduction of the issue +- Step-by-step instructions to reproduce the issue +- Ideally a reproduction repository +- Impact of the issue, including how an attacker might exploit the issue + +We prefer to receive reports in English. + +## Contact + +Please disclose a vulnerability or security relevant issue here: [https://github.com/tauri-apps/plugins-workspace/security/advisories/new](https://github.com/tauri-apps/plugins-workspace/security/advisories/new). + +Alternatively, you can also contact us by email via [security@tauri.app](mailto:security@tauri.app). diff --git a/plugins/opener/android/.gitignore b/plugins/opener/android/.gitignore new file mode 100644 index 0000000..c0f21ec --- /dev/null +++ b/plugins/opener/android/.gitignore @@ -0,0 +1,2 @@ +/build +/.tauri diff --git a/plugins/opener/android/build.gradle.kts b/plugins/opener/android/build.gradle.kts new file mode 100644 index 0000000..9ed0957 --- /dev/null +++ b/plugins/opener/android/build.gradle.kts @@ -0,0 +1,31 @@ +plugins { + id("com.android.library") + id("org.jetbrains.kotlin.android") +} + +android { + namespace = "app.tauri.opener" + compileSdk = 36 + + defaultConfig { + minSdk = 24 + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + consumerProguardFiles("consumer-rules.pro") + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 + } + kotlinOptions { + jvmTarget = "1.8" + } +} + +dependencies { + implementation("androidx.core:core-ktx:1.9.0") + implementation("com.fasterxml.jackson.core:jackson-databind:2.15.3") + implementation("androidx.browser:browser:1.8.0") + implementation(project(":tauri-android")) +} diff --git a/plugins/opener/android/consumer-rules.pro b/plugins/opener/android/consumer-rules.pro new file mode 100644 index 0000000..481bb43 --- /dev/null +++ b/plugins/opener/android/consumer-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/plugins/opener/android/settings.gradle b/plugins/opener/android/settings.gradle new file mode 100644 index 0000000..14a752e --- /dev/null +++ b/plugins/opener/android/settings.gradle @@ -0,0 +1,2 @@ +include ':tauri-android' +project(':tauri-android').projectDir = new File('./.tauri/tauri-api') diff --git a/plugins/opener/android/src/main/AndroidManifest.xml b/plugins/opener/android/src/main/AndroidManifest.xml new file mode 100644 index 0000000..9a40236 --- /dev/null +++ b/plugins/opener/android/src/main/AndroidManifest.xml @@ -0,0 +1,3 @@ + + + diff --git a/plugins/opener/android/src/main/java/OpenerPlugin.kt b/plugins/opener/android/src/main/java/OpenerPlugin.kt new file mode 100644 index 0000000..a65a2f5 --- /dev/null +++ b/plugins/opener/android/src/main/java/OpenerPlugin.kt @@ -0,0 +1,44 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +package app.tauri.opener + +import android.app.Activity +import android.content.Intent +import androidx.browser.customtabs.CustomTabsIntent +import app.tauri.annotation.Command +import app.tauri.annotation.TauriPlugin +import app.tauri.plugin.Invoke +import app.tauri.plugin.Plugin +import androidx.core.net.toUri +import app.tauri.annotation.InvokeArg + +@InvokeArg +class OpenArgs { + lateinit var url: String + var with: String? = null +} + +@TauriPlugin +class OpenerPlugin(private val activity: Activity) : Plugin(activity) { + @Command + fun open(invoke: Invoke) { + try { + val args = invoke.parseArgs(OpenArgs::class.java) + + if (args.with == "inAppBrowser") { + val builder = CustomTabsIntent.Builder() + val intent = builder.build() + intent.launchUrl(activity, args.url.toUri()) + } else { + val intent = Intent(Intent.ACTION_VIEW, args.url.toUri()) + intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + activity.applicationContext?.startActivity(intent) + } + invoke.resolve() + } catch (ex: Exception) { + invoke.reject(ex.message) + } + } +} diff --git a/plugins/opener/api-iife.js b/plugins/opener/api-iife.js new file mode 100644 index 0000000..57393c5 --- /dev/null +++ b/plugins/opener/api-iife.js @@ -0,0 +1 @@ +if("__TAURI__"in window){var __TAURI_PLUGIN_OPENER__=function(n,e){"use strict";return n.openPath=async function(n,_){await e.invoke("plugin:opener|open_path",{path:n,with:_})},n.openUrl=async function(n,_){await e.invoke("plugin:opener|open_url",{url:n,with:_})},n.revealItemInDir=async function(n){const _="string"==typeof n?[n]:n;return e.invoke("plugin:opener|reveal_item_in_dir",{paths:_})},n}({},window.__TAURI__.core);Object.defineProperty(window.__TAURI__,"opener",{value:__TAURI_PLUGIN_OPENER__})} diff --git a/plugins/opener/build.rs b/plugins/opener/build.rs new file mode 100644 index 0000000..6769035 --- /dev/null +++ b/plugins/opener/build.rs @@ -0,0 +1,132 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use std::path::PathBuf; + +#[path = "src/scope_entry.rs"] +#[allow(dead_code)] +mod scope; + +/// Opener scope application. +#[derive(schemars::JsonSchema)] +#[serde(untagged)] +#[allow(unused)] +#[derive(Default)] +enum Application { + /// Open in default application. + #[default] + Default, + /// If true, allow open with any application. + Enable(bool), + /// Allow specific application to open with. + App(String), +} + +/// Opener scope entry. +#[derive(schemars::JsonSchema)] +#[serde(untagged)] +#[allow(unused)] +enum OpenerScopeEntry { + Url { + /// A URL that can be opened by the webview when using the Opener APIs. + /// + /// Wildcards can be used following the UNIX glob pattern. + /// + /// Examples: + /// + /// - "https://*" : allows all HTTPS origin + /// + /// - "https://*.github.com/tauri-apps/tauri": allows any subdomain of "github.com" with the "tauri-apps/api" path + /// + /// - "https://myapi.service.com/users/*": allows access to any URLs that begins with "https://myapi.service.com/users/" + url: String, + /// An application to open this url with, for example: firefox. + #[serde(default)] + app: Application, + }, + Path { + /// A path that can be opened by the webview when using the Opener APIs. + /// + /// The pattern can start with a variable that resolves to a system base directory. + /// The variables are: `$AUDIO`, `$CACHE`, `$CONFIG`, `$DATA`, `$LOCALDATA`, `$DESKTOP`, + /// `$DOCUMENT`, `$DOWNLOAD`, `$EXE`, `$FONT`, `$HOME`, `$PICTURE`, `$PUBLIC`, `$RUNTIME`, + /// `$TEMPLATE`, `$VIDEO`, `$RESOURCE`, `$APP`, `$LOG`, `$TEMP`, `$APPCONFIG`, `$APPDATA`, + /// `$APPLOCALDATA`, `$APPCACHE`, `$APPLOG`. + path: PathBuf, + /// An application to open this path with, for example: xdg-open. + #[serde(default)] + app: Application, + }, +} + +// Ensure `OpenerScopeEntry` and `scope::EntryRaw` is kept in sync +fn _f() { + match (scope::EntryRaw::Url { + url: String::new(), + app: scope::Application::Enable(true), + }) { + scope::EntryRaw::Url { url, app } => OpenerScopeEntry::Url { + url, + app: match app { + scope::Application::Enable(p) => Application::Enable(p), + scope::Application::App(p) => Application::App(p), + scope::Application::Default => Application::Default, + }, + }, + scope::EntryRaw::Path { path, app } => OpenerScopeEntry::Path { + path, + app: match app { + scope::Application::Enable(p) => Application::Enable(p), + scope::Application::App(p) => Application::App(p), + scope::Application::Default => Application::Default, + }, + }, + }; + match (OpenerScopeEntry::Url { + url: String::new(), + app: Application::Enable(true), + }) { + OpenerScopeEntry::Url { url, app } => scope::EntryRaw::Url { + url, + app: match app { + Application::Enable(p) => scope::Application::Enable(p), + Application::App(p) => scope::Application::App(p), + Application::Default => scope::Application::Default, + }, + }, + OpenerScopeEntry::Path { path, app } => scope::EntryRaw::Path { + path, + app: match app { + Application::Enable(p) => scope::Application::Enable(p), + Application::App(p) => scope::Application::App(p), + Application::Default => scope::Application::Default, + }, + }, + }; +} + +const COMMANDS: &[&str] = &["open_url", "open_path", "reveal_item_in_dir"]; + +fn main() { + tauri_plugin::Builder::new(COMMANDS) + .global_api_script_path("./api-iife.js") + .android_path("android") + .ios_path("ios") + .global_scope_schema(schemars::schema_for!(OpenerScopeEntry)) + .build(); + + let target_os = std::env::var("CARGO_CFG_TARGET_OS").unwrap(); + let mobile = target_os == "ios" || target_os == "android"; + alias("desktop", !mobile); + alias("mobile", mobile); +} + +// creates a cfg alias if `has_feature` is true. +// `alias` must be a snake case string. +fn alias(alias: &str, has_feature: bool) { + println!("cargo:rustc-check-cfg=cfg({alias})"); + if has_feature { + println!("cargo:rustc-cfg={alias}"); + } +} diff --git a/plugins/opener/guest-js/index.ts b/plugins/opener/guest-js/index.ts new file mode 100644 index 0000000..6b40da1 --- /dev/null +++ b/plugins/opener/guest-js/index.ts @@ -0,0 +1,99 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +/** + * Open files and URLs using their default application. + * + * ## Security + * + * This API has a scope configuration that forces you to restrict the files and urls to be opened. + * + * ### Restricting access to the {@link open | `open`} API + * + * On the configuration object, `open: true` means that the {@link open} API can be used with any URL, + * as the argument is validated with the `^((mailto:\w+)|(tel:\w+)|(https?://\w+)).+` regex. + * You can change that regex by changing the boolean value to a string, e.g. `open: ^https://github.com/`. + * + * @module + */ + +import { invoke } from '@tauri-apps/api/core' + +/** + * Opens a url with the system's default app, or the one specified with {@linkcode openWith}. + * + * @example + * ```typescript + * import { openUrl } from '@tauri-apps/plugin-opener'; + * + * // opens the given URL on the default browser: + * await openUrl('https://github.com/tauri-apps/tauri'); + * // opens the given URL using `firefox`: + * await openUrl('https://github.com/tauri-apps/tauri', 'firefox'); + * ``` + * + * @param url The URL to open. + * @param openWith The app to open the URL with. If not specified, defaults to the system default application for the specified url type. + * On mobile, `openWith` can be provided as `inAppBrowser` to open the URL in an in-app browser. Otherwise, it will open the URL in the system default browser. + * + * @since 2.0.0 + */ +export async function openUrl( + url: string | URL, + // eslint-disable-next-line @typescript-eslint/no-redundant-type-constituents + openWith?: 'inAppBrowser' | string +): Promise { + await invoke('plugin:opener|open_url', { + url, + with: openWith + }) +} + +/** + * Opens a path with the system's default app, or the one specified with {@linkcode openWith}. + * + * @example + * ```typescript + * import { openPath } from '@tauri-apps/plugin-opener'; + * + * // opens a file using the default program: + * await openPath('/path/to/file'); + * // opens a file using `vlc` command on Windows. + * await openPath('C:/path/to/file', 'vlc'); + * ``` + * + * @param path The path to open. + * @param openWith The app to open the path with. If not specified, defaults to the system default application for the specified path type. + * + * @since 2.0.0 + */ +export async function openPath(path: string, openWith?: string): Promise { + await invoke('plugin:opener|open_path', { + path, + with: openWith + }) +} + +/** + * Reveal a path with the system's default explorer. + * + * #### Platform-specific: + * + * - **Android / iOS:** Unsupported. + * + * @example + * ```typescript + * import { revealItemInDir } from '@tauri-apps/plugin-opener'; + * await revealItemInDir('/path/to/file'); + * await revealItemInDir([ '/path/to/file', '/path/to/another/file' ]); + * ``` + * + * @param path The path to reveal. + * + * @since 2.0.0 + */ +export async function revealItemInDir(path: string | string[]): Promise { + const paths = typeof path === 'string' ? [path] : path + return invoke('plugin:opener|reveal_item_in_dir', { paths }) +} diff --git a/plugins/opener/guest-js/init.ts b/plugins/opener/guest-js/init.ts new file mode 100644 index 0000000..d012386 --- /dev/null +++ b/plugins/opener/guest-js/init.ts @@ -0,0 +1,70 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +/** + * Webview script injected by the opener plugin on every page load. + * + * It installs a `click` listener on `window` that intercepts clicks on `
` elements whose + * `target` is `_blank` (or that are clicked while holding `Ctrl` or `Shift`) and whose `href` + * uses the `http:`, `https:`, `mailto:` or `tel:` protocol, cancels the navigation and opens the + * link with the system's default browser through the `plugin:opener|open_url` command instead. + * + * @module + */ + +import { invoke } from '@tauri-apps/api/core' + +// open links with the API +window.addEventListener('click', function (evt) { + // return early if + if ( + // event was prevented + evt.defaultPrevented + // or not a left click + || evt.button !== 0 + // or meta key pressed + || evt.metaKey + // or al key pressed + || evt.altKey + ) + return + + const a = evt + .composedPath() + .find((el) => el instanceof Node && el.nodeName.toUpperCase() === 'A') as + | HTMLAnchorElement + | undefined + + // return early if + if ( + // not tirggered from element + !a + // or doesn't have a href + || !a.href + // or not supposed to be open in a new tab + || !( + a.target === '_blank' + // or ctrl key pressed + || evt.ctrlKey + // or shift key pressed + || evt.shiftKey + ) + ) + return + + const url = new URL(a.href) + + // return early if + if ( + // not default protocols + ['http:', 'https:', 'mailto:', 'tel:'].every((p) => url.protocol !== p) + ) + return + + evt.preventDefault() + + void invoke('plugin:opener|open_url', { + url + }) +}) diff --git a/plugins/opener/ios/Package.resolved b/plugins/opener/ios/Package.resolved new file mode 100644 index 0000000..5f998e0 --- /dev/null +++ b/plugins/opener/ios/Package.resolved @@ -0,0 +1,16 @@ +{ + "object": { + "pins": [ + { + "package": "SwiftRs", + "repositoryURL": "https://github.com/Brendonovich/swift-rs", + "state": { + "branch": null, + "revision": "b5ed223fcdab165bc21219c1925dc1e77e2bef5e", + "version": "1.0.6" + } + } + ] + }, + "version": 1 +} diff --git a/plugins/opener/ios/Package.swift b/plugins/opener/ios/Package.swift new file mode 100644 index 0000000..8d06936 --- /dev/null +++ b/plugins/opener/ios/Package.swift @@ -0,0 +1,34 @@ +// swift-tools-version:5.3 +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +import PackageDescription + +let package = Package( + name: "tauri-plugin-opener", + platforms: [ + .macOS(.v10_13), + .iOS(.v13), + ], + products: [ + // Products define the executables and libraries a package produces, and make them visible to other packages. + .library( + name: "tauri-plugin-opener", + type: .static, + targets: ["tauri-plugin-opener"]) + ], + dependencies: [ + .package(name: "Tauri", path: "../.tauri/tauri-api") + ], + targets: [ + // Targets are the basic building blocks of a package. A target can define a module or a test suite. + // Targets can depend on other targets in this package, and on products in packages this package depends on. + .target( + name: "tauri-plugin-opener", + dependencies: [ + .byName(name: "Tauri") + ], + path: "Sources") + ] +) diff --git a/plugins/opener/ios/Sources/OpenerPlugin.swift b/plugins/opener/ios/Sources/OpenerPlugin.swift new file mode 100644 index 0000000..2311a3b --- /dev/null +++ b/plugins/opener/ios/Sources/OpenerPlugin.swift @@ -0,0 +1,46 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +import Foundation +import SafariServices +import SwiftRs +import Tauri +import UIKit +import WebKit + +struct OpenArgs: Decodable { + let url: String + let with: String? +} + +class OpenerPlugin: Plugin { + @objc public func open(_ invoke: Invoke) throws { + do { + let args = try invoke.parseArgs(OpenArgs.self) + if let url = URL(string: args.url) { + if args.with == "inAppBrowser" { + DispatchQueue.main.async { + let safariVC = SFSafariViewController(url: url) + self.manager.viewController?.present(safariVC, animated: true) + } + } else { + if #available(iOS 10, *) { + UIApplication.shared.open(url, options: [:]) + } else { + UIApplication.shared.openURL(url) + } + } + + } + invoke.resolve() + } catch { + invoke.reject(error.localizedDescription) + } + } +} + +@_cdecl("init_plugin_opener") +func initPlugin() -> Plugin { + return OpenerPlugin() +} diff --git a/plugins/opener/package.json b/plugins/opener/package.json new file mode 100644 index 0000000..f51f27a --- /dev/null +++ b/plugins/opener/package.json @@ -0,0 +1,30 @@ +{ + "name": "@tauri-apps/plugin-opener", + "version": "2.5.5", + "description": "Open files and URLs using their default application.", + "license": "MIT OR Apache-2.0", + "authors": [ + "Tauri Programme within The Commons Conservancy" + ], + "repository": "https://github.com/tauri-apps/plugins-workspace", + "type": "module", + "types": "./dist-js/index.d.ts", + "main": "./dist-js/index.cjs", + "module": "./dist-js/index.js", + "exports": { + "types": "./dist-js/index.d.ts", + "import": "./dist-js/index.js", + "require": "./dist-js/index.cjs" + }, + "scripts": { + "build": "rollup -c" + }, + "files": [ + "dist-js", + "README.md", + "LICENSE" + ], + "dependencies": { + "@tauri-apps/api": "^2.11.0" + } +} diff --git a/plugins/opener/permissions/allow-default-urls.toml b/plugins/opener/permissions/allow-default-urls.toml new file mode 100644 index 0000000..93495b8 --- /dev/null +++ b/plugins/opener/permissions/allow-default-urls.toml @@ -0,0 +1,17 @@ +"$schema" = "schemas/schema.json" + +[[permission]] +identifier = "allow-default-urls" +description = "This enables opening `mailto:`, `tel:`, `https://` and `http://` urls using their default application." + +[[permission.scope.allow]] +url = "mailto:*" + +[[permission.scope.allow]] +url = "tel:*" + +[[permission.scope.allow]] +url = "http://*" + +[[permission.scope.allow]] +url = "https://*" diff --git a/plugins/opener/permissions/autogenerated/commands/open_path.toml b/plugins/opener/permissions/autogenerated/commands/open_path.toml new file mode 100644 index 0000000..ae67b93 --- /dev/null +++ b/plugins/opener/permissions/autogenerated/commands/open_path.toml @@ -0,0 +1,13 @@ +# Automatically generated - DO NOT EDIT! + +"$schema" = "../../schemas/schema.json" + +[[permission]] +identifier = "allow-open-path" +description = "Enables the open_path command without any pre-configured scope." +commands.allow = ["open_path"] + +[[permission]] +identifier = "deny-open-path" +description = "Denies the open_path command without any pre-configured scope." +commands.deny = ["open_path"] diff --git a/plugins/opener/permissions/autogenerated/commands/open_url.toml b/plugins/opener/permissions/autogenerated/commands/open_url.toml new file mode 100644 index 0000000..f1e694b --- /dev/null +++ b/plugins/opener/permissions/autogenerated/commands/open_url.toml @@ -0,0 +1,13 @@ +# Automatically generated - DO NOT EDIT! + +"$schema" = "../../schemas/schema.json" + +[[permission]] +identifier = "allow-open-url" +description = "Enables the open_url command without any pre-configured scope." +commands.allow = ["open_url"] + +[[permission]] +identifier = "deny-open-url" +description = "Denies the open_url command without any pre-configured scope." +commands.deny = ["open_url"] diff --git a/plugins/opener/permissions/autogenerated/commands/reveal_item_in_dir.toml b/plugins/opener/permissions/autogenerated/commands/reveal_item_in_dir.toml new file mode 100644 index 0000000..e669620 --- /dev/null +++ b/plugins/opener/permissions/autogenerated/commands/reveal_item_in_dir.toml @@ -0,0 +1,13 @@ +# Automatically generated - DO NOT EDIT! + +"$schema" = "../../schemas/schema.json" + +[[permission]] +identifier = "allow-reveal-item-in-dir" +description = "Enables the reveal_item_in_dir command without any pre-configured scope." +commands.allow = ["reveal_item_in_dir"] + +[[permission]] +identifier = "deny-reveal-item-in-dir" +description = "Denies the reveal_item_in_dir command without any pre-configured scope." +commands.deny = ["reveal_item_in_dir"] diff --git a/plugins/opener/permissions/autogenerated/reference.md b/plugins/opener/permissions/autogenerated/reference.md new file mode 100644 index 0000000..6ad6ba1 --- /dev/null +++ b/plugins/opener/permissions/autogenerated/reference.md @@ -0,0 +1,111 @@ +## Default Permission + +This permission set allows opening `mailto:`, `tel:`, `https://` and `http://` urls using their default application +as well as reveal file in directories using default file explorer + +#### This default permission set includes the following: + +- `allow-open-url` +- `allow-reveal-item-in-dir` +- `allow-default-urls` + +## Permission Table + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
IdentifierDescription
+ +`opener:allow-default-urls` + + + +This enables opening `mailto:`, `tel:`, `https://` and `http://` urls using their default application. + +
+ +`opener:allow-open-path` + + + +Enables the open_path command without any pre-configured scope. + +
+ +`opener:deny-open-path` + + + +Denies the open_path command without any pre-configured scope. + +
+ +`opener:allow-open-url` + + + +Enables the open_url command without any pre-configured scope. + +
+ +`opener:deny-open-url` + + + +Denies the open_url command without any pre-configured scope. + +
+ +`opener:allow-reveal-item-in-dir` + + + +Enables the reveal_item_in_dir command without any pre-configured scope. + +
+ +`opener:deny-reveal-item-in-dir` + + + +Denies the reveal_item_in_dir command without any pre-configured scope. + +
diff --git a/plugins/opener/permissions/default.toml b/plugins/opener/permissions/default.toml new file mode 100644 index 0000000..846d6e5 --- /dev/null +++ b/plugins/opener/permissions/default.toml @@ -0,0 +1,10 @@ +"$schema" = "schemas/schema.json" + +[default] +description = """This permission set allows opening `mailto:`, `tel:`, `https://` and `http://` urls using their default application +as well as reveal file in directories using default file explorer""" +permissions = [ + "allow-open-url", + "allow-reveal-item-in-dir", + "allow-default-urls", +] diff --git a/plugins/opener/permissions/schemas/schema.json b/plugins/opener/permissions/schemas/schema.json new file mode 100644 index 0000000..78c80ba --- /dev/null +++ b/plugins/opener/permissions/schemas/schema.json @@ -0,0 +1,348 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "PermissionFile", + "description": "Permission file that can define a default permission, a set of permissions or a list of inlined permissions.", + "type": "object", + "properties": { + "default": { + "description": "The default permission set for the plugin", + "anyOf": [ + { + "$ref": "#/definitions/DefaultPermission" + }, + { + "type": "null" + } + ] + }, + "set": { + "description": "A list of permissions sets defined", + "type": "array", + "items": { + "$ref": "#/definitions/PermissionSet" + } + }, + "permission": { + "description": "A list of inlined permissions", + "default": [], + "type": "array", + "items": { + "$ref": "#/definitions/Permission" + } + } + }, + "definitions": { + "DefaultPermission": { + "description": "The default permission set of the plugin.\n\nWorks similarly to a permission with the \"default\" identifier.", + "type": "object", + "required": [ + "permissions" + ], + "properties": { + "version": { + "description": "The version of the permission.", + "type": [ + "integer", + "null" + ], + "format": "uint64", + "minimum": 1.0 + }, + "description": { + "description": "Human-readable description of what the permission does. Tauri convention is to use `

` headings in markdown content for Tauri documentation generation purposes.", + "type": [ + "string", + "null" + ] + }, + "permissions": { + "description": "All permissions this set contains.", + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "PermissionSet": { + "description": "A set of direct permissions grouped together under a new name.", + "type": "object", + "required": [ + "description", + "identifier", + "permissions" + ], + "properties": { + "identifier": { + "description": "A unique identifier for the permission.", + "type": "string" + }, + "description": { + "description": "Human-readable description of what the permission does.", + "type": "string" + }, + "permissions": { + "description": "All permissions this set contains.", + "type": "array", + "items": { + "$ref": "#/definitions/PermissionKind" + } + } + } + }, + "Permission": { + "description": "Descriptions of explicit privileges of commands.\n\nIt can enable commands to be accessible in the frontend of the application.\n\nIf the scope is defined it can be used to fine grain control the access of individual or multiple commands.", + "type": "object", + "required": [ + "identifier" + ], + "properties": { + "version": { + "description": "The version of the permission.", + "type": [ + "integer", + "null" + ], + "format": "uint64", + "minimum": 1.0 + }, + "identifier": { + "description": "A unique identifier for the permission.", + "type": "string" + }, + "description": { + "description": "Human-readable description of what the permission does. Tauri internal convention is to use `

` headings in markdown content for Tauri documentation generation purposes.", + "type": [ + "string", + "null" + ] + }, + "commands": { + "description": "Allowed or denied commands when using this permission.", + "default": { + "allow": [], + "deny": [] + }, + "allOf": [ + { + "$ref": "#/definitions/Commands" + } + ] + }, + "scope": { + "description": "Allowed or denied scoped when using this permission.", + "allOf": [ + { + "$ref": "#/definitions/Scopes" + } + ] + }, + "platforms": { + "description": "Target platforms this permission applies. By default all platforms are affected by this permission.", + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/Target" + } + } + } + }, + "Commands": { + "description": "Allowed and denied commands inside a permission.\n\nIf two commands clash inside of `allow` and `deny`, it should be denied by default.", + "type": "object", + "properties": { + "allow": { + "description": "Allowed command.", + "default": [], + "type": "array", + "items": { + "type": "string" + } + }, + "deny": { + "description": "Denied command, which takes priority.", + "default": [], + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "Scopes": { + "description": "An argument for fine grained behavior control of Tauri commands.\n\nIt can be of any serde serializable type and is used to allow or prevent certain actions inside a Tauri command. The configured scope is passed to the command and will be enforced by the command implementation.\n\n## Example\n\n```json { \"allow\": [{ \"path\": \"$HOME/**\" }], \"deny\": [{ \"path\": \"$HOME/secret.txt\" }] } ```", + "type": "object", + "properties": { + "allow": { + "description": "Data that defines what is allowed by the scope.", + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/Value" + } + }, + "deny": { + "description": "Data that defines what is denied by the scope. This should be prioritized by validation logic.", + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/Value" + } + } + } + }, + "Value": { + "description": "All supported ACL values.", + "anyOf": [ + { + "description": "Represents a null JSON value.", + "type": "null" + }, + { + "description": "Represents a [`bool`].", + "type": "boolean" + }, + { + "description": "Represents a valid ACL [`Number`].", + "allOf": [ + { + "$ref": "#/definitions/Number" + } + ] + }, + { + "description": "Represents a [`String`].", + "type": "string" + }, + { + "description": "Represents a list of other [`Value`]s.", + "type": "array", + "items": { + "$ref": "#/definitions/Value" + } + }, + { + "description": "Represents a map of [`String`] keys to [`Value`]s.", + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/Value" + } + } + ] + }, + "Number": { + "description": "A valid ACL number.", + "anyOf": [ + { + "description": "Represents an [`i64`].", + "type": "integer", + "format": "int64" + }, + { + "description": "Represents a [`f64`].", + "type": "number", + "format": "double" + } + ] + }, + "Target": { + "description": "Platform target.", + "oneOf": [ + { + "description": "MacOS.", + "type": "string", + "enum": [ + "macOS" + ] + }, + { + "description": "Windows.", + "type": "string", + "enum": [ + "windows" + ] + }, + { + "description": "Linux.", + "type": "string", + "enum": [ + "linux" + ] + }, + { + "description": "Android.", + "type": "string", + "enum": [ + "android" + ] + }, + { + "description": "iOS.", + "type": "string", + "enum": [ + "iOS" + ] + } + ] + }, + "PermissionKind": { + "type": "string", + "oneOf": [ + { + "description": "This enables opening `mailto:`, `tel:`, `https://` and `http://` urls using their default application.", + "type": "string", + "const": "allow-default-urls", + "markdownDescription": "This enables opening `mailto:`, `tel:`, `https://` and `http://` urls using their default application." + }, + { + "description": "Enables the open_path command without any pre-configured scope.", + "type": "string", + "const": "allow-open-path", + "markdownDescription": "Enables the open_path command without any pre-configured scope." + }, + { + "description": "Denies the open_path command without any pre-configured scope.", + "type": "string", + "const": "deny-open-path", + "markdownDescription": "Denies the open_path command without any pre-configured scope." + }, + { + "description": "Enables the open_url command without any pre-configured scope.", + "type": "string", + "const": "allow-open-url", + "markdownDescription": "Enables the open_url command without any pre-configured scope." + }, + { + "description": "Denies the open_url command without any pre-configured scope.", + "type": "string", + "const": "deny-open-url", + "markdownDescription": "Denies the open_url command without any pre-configured scope." + }, + { + "description": "Enables the reveal_item_in_dir command without any pre-configured scope.", + "type": "string", + "const": "allow-reveal-item-in-dir", + "markdownDescription": "Enables the reveal_item_in_dir command without any pre-configured scope." + }, + { + "description": "Denies the reveal_item_in_dir command without any pre-configured scope.", + "type": "string", + "const": "deny-reveal-item-in-dir", + "markdownDescription": "Denies the reveal_item_in_dir command without any pre-configured scope." + }, + { + "description": "This permission set allows opening `mailto:`, `tel:`, `https://` and `http://` urls using their default application\nas well as reveal file in directories using default file explorer\n#### This default permission set includes:\n\n- `allow-open-url`\n- `allow-reveal-item-in-dir`\n- `allow-default-urls`", + "type": "string", + "const": "default", + "markdownDescription": "This permission set allows opening `mailto:`, `tel:`, `https://` and `http://` urls using their default application\nas well as reveal file in directories using default file explorer\n#### This default permission set includes:\n\n- `allow-open-url`\n- `allow-reveal-item-in-dir`\n- `allow-default-urls`" + } + ] + } + } +} \ No newline at end of file diff --git a/plugins/opener/rollup.config.js b/plugins/opener/rollup.config.js new file mode 100644 index 0000000..a7dbd4f --- /dev/null +++ b/plugins/opener/rollup.config.js @@ -0,0 +1,22 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +import { createConfig } from '../../shared/rollup.config.js' +import { nodeResolve } from '@rollup/plugin-node-resolve' +import typescript from '@rollup/plugin-typescript' +import terser from '@rollup/plugin-terser' + +export default createConfig({ + additionalConfigs: { + input: 'guest-js/init.ts', + output: { + file: 'src/init-iife.js', + format: 'iife' + }, + plugins: [typescript(), terser(), nodeResolve()], + onwarn: (warning) => { + throw Object.assign(new Error(), warning) + } + } +}) diff --git a/plugins/opener/src/commands.rs b/plugins/opener/src/commands.rs new file mode 100644 index 0000000..e22b41d --- /dev/null +++ b/plugins/opener/src/commands.rs @@ -0,0 +1,76 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use std::path::{Path, PathBuf}; + +use tauri::{ + ipc::{CommandScope, GlobalScope}, + AppHandle, Runtime, +}; + +use crate::{scope::Scope, Error, OpenerExt}; + +#[tauri::command] +pub async fn open_url( + app: AppHandle, + command_scope: CommandScope, + global_scope: GlobalScope, + url: String, + with: Option, +) -> crate::Result<()> { + let scope = Scope::new( + &app, + command_scope + .allows() + .iter() + .chain(global_scope.allows()) + .collect(), + command_scope + .denies() + .iter() + .chain(global_scope.denies()) + .collect(), + ); + + if scope.is_url_allowed(&url, with.as_deref()) { + app.opener().open_url(url, with) + } else { + Err(Error::ForbiddenUrl { url, with }) + } +} + +#[tauri::command] +pub async fn open_path( + app: AppHandle, + command_scope: CommandScope, + global_scope: GlobalScope, + path: String, + with: Option, +) -> crate::Result<()> { + let scope = Scope::new( + &app, + command_scope + .allows() + .iter() + .chain(global_scope.allows()) + .collect(), + command_scope + .denies() + .iter() + .chain(global_scope.denies()) + .collect(), + ); + + if scope.is_path_allowed(Path::new(&path), with.as_deref())? { + app.opener().open_path(path, with) + } else { + Err(Error::ForbiddenPath { path, with }) + } +} + +/// TODO: in the next major version, rename to `reveal_items_in_dir` +#[tauri::command] +pub async fn reveal_item_in_dir(paths: Vec) -> crate::Result<()> { + crate::reveal_items_in_dir(&paths) +} diff --git a/plugins/opener/src/config.rs b/plugins/opener/src/config.rs new file mode 100644 index 0000000..db3bae4 --- /dev/null +++ b/plugins/opener/src/config.rs @@ -0,0 +1,19 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use serde::Deserialize; + +#[derive(Deserialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct Config { + /// Whether or not paths that contain components that start with a `.` + /// will require that `.` appears literally in the pattern; `*`, `?`, `**`, + /// or `[...]` will not match. This is useful because such files are + /// conventionally considered hidden on Unix systems and it might be + /// desirable to skip them when listing files. + /// + /// Defaults to `true` on Unix systems and `false` on Windows + // dotfiles are not supposed to be exposed by default on unix + pub require_literal_leading_dot: Option, +} diff --git a/plugins/opener/src/error.rs b/plugins/opener/src/error.rs new file mode 100644 index 0000000..05642fb --- /dev/null +++ b/plugins/opener/src/error.rs @@ -0,0 +1,82 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use std::path::PathBuf; + +use serde::{Serialize, Serializer}; + +/// The error type returned by the opener plugin's commands and APIs. +#[derive(Debug, thiserror::Error)] +#[non_exhaustive] +pub enum Error { + /// Forwarded from a failed call into the mobile plugin runtime. + #[cfg(mobile)] + #[error(transparent)] + PluginInvoke(#[from] tauri::plugin::mobile::PluginInvokeError), + /// Forwarded from a [`tauri::Error`], e.g. when resolving a scoped path fails. + #[error(transparent)] + Tauri(#[from] tauri::Error), + /// Forwarded from an [`std::io::Error`] raised while opening or revealing a path. + #[error(transparent)] + Io(#[from] std::io::Error), + /// Forwarded from a [`serde_json::Error`] raised while (de)serializing a mobile plugin payload. + #[error(transparent)] + Json(#[from] serde_json::Error), + /// The program passed as `with` is not a program name known by the underlying opener. + #[error("unknown program {0}")] + UnknownProgramName(String), + /// The path is not allowed by the opener scope, optionally together with the program it was requested to be opened with. + #[error("Not allowed to open path {}{}", .path, .with.as_ref().map(|w| format!(" with {w}")).unwrap_or_default())] + ForbiddenPath { + /// The path that was rejected by the scope. + path: String, + /// The program the path was requested to be opened with, if any. + with: Option, + }, + /// The URL is not allowed by the opener scope, optionally together with the program it was requested to be opened with. + #[error("Not allowed to open url {}{}", .url, .with.as_ref().map(|w| format!(" with {w}")).unwrap_or_default())] + ForbiddenUrl { + /// The URL that was rejected by the scope. + url: String, + /// The program the URL was requested to be opened with, if any. + with: Option, + }, + /// The requested API is not supported on the current platform, e.g. [`crate::reveal_item_in_dir`] on Android and iOS. + #[error("API not supported on the current platform")] + UnsupportedPlatform, + /// Forwarded from a Win32 API call, see [`windows::core::Error`]. + #[error(transparent)] + #[cfg(windows)] + Win32Error(#[from] windows::core::Error), + /// The given path has no parent directory, so it cannot be revealed in its containing folder. + #[error("Path doesn't have a parent: {0}")] + NoParent(PathBuf), + // TODO: Add the underlying io::Error to this variant + /// Failed to convert the path to a Windows `ITEMIDLIST` while preparing it to be revealed in the file explorer. + #[cfg(windows)] + #[error("Failed to convert path '{0}' to ITEMIDLIST")] + FailedToConvertPathToItemIdList(PathBuf), + /// Failed to convert the path to a `file://` URL, which is required to reveal it via D-Bus on Linux and BSD. + #[error("Failed to convert path to file:// url")] + FailedToConvertPathToFileUrl, + /// Forwarded from a [`zbus::Error`] raised while talking to the file manager or the desktop portal over D-Bus. + #[error(transparent)] + #[cfg(any( + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd" + ))] + Zbus(#[from] zbus::Error), +} + +impl Serialize for Error { + fn serialize(&self, serializer: S) -> std::result::Result + where + S: Serializer, + { + serializer.serialize_str(self.to_string().as_ref()) + } +} diff --git a/plugins/opener/src/init-iife.js b/plugins/opener/src/init-iife.js new file mode 100644 index 0000000..02c66a6 --- /dev/null +++ b/plugins/opener/src/init-iife.js @@ -0,0 +1 @@ +!function(){"use strict";"function"==typeof SuppressedError&&SuppressedError,window.addEventListener("click",function(e){if(e.defaultPrevented||0!==e.button||e.metaKey||e.altKey)return;const t=e.composedPath().find(e=>e instanceof Node&&"A"===e.nodeName.toUpperCase());if(!t||!t.href||"_blank"!==t.target&&!e.ctrlKey&&!e.shiftKey)return;const n=new URL(t.href);["http:","https:","mailto:","tel:"].every(e=>n.protocol!==e)||(e.preventDefault(),async function(e,t={},n){window.__TAURI_INTERNALS__.invoke(e,t,n)}("plugin:opener|open_url",{url:n}))})}(); diff --git a/plugins/opener/src/lib.rs b/plugins/opener/src/lib.rs new file mode 100644 index 0000000..13f6efb --- /dev/null +++ b/plugins/opener/src/lib.rs @@ -0,0 +1,269 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +//! Open files and URLs using their default application, and reveal files in the system's file explorer. +//! +//! Use the [`OpenerExt`] trait to access [`Opener::open_url`] and [`Opener::open_path`] from a +//! running Tauri app; the plugin's `open_url` and `open_path` commands enforce the scope +//! configured for the plugin before delegating to them. The [`open_url`] and [`open_path`] free +//! functions and the [`Opener`] methods themselves do not perform any scope check. +//! [`reveal_item_in_dir`] and [`reveal_items_in_dir`] return [`Error::UnsupportedPlatform`] on +//! Android and iOS. + +use std::path::Path; + +use tauri::{plugin::TauriPlugin, Manager, Runtime}; + +#[cfg(mobile)] +use tauri::plugin::PluginHandle; +#[cfg(target_os = "android")] +const PLUGIN_IDENTIFIER: &str = "app.tauri.opener"; +#[cfg(target_os = "ios")] +tauri::ios_plugin_binding!(init_plugin_opener); + +mod commands; +mod config; +mod error; +mod open; +mod reveal_item_in_dir; +mod scope; +mod scope_entry; +#[cfg(windows)] +mod windows_shell_path; + +pub use error::Error; +type Result = std::result::Result; + +pub use open::{open_path, open_url}; +pub use reveal_item_in_dir::{reveal_item_in_dir, reveal_items_in_dir}; + +/// Access to the opener APIs, managed by the plugin as app state. +/// +/// Obtain an instance via [`OpenerExt::opener`]. +pub struct Opener { + // we use `fn() -> R` to silence the unused generic error + // while keeping this struct `Send + Sync` without requiring `R` to be + #[cfg(not(mobile))] + _marker: std::marker::PhantomData R>, + #[cfg(mobile)] + mobile_plugin_handle: PluginHandle, + require_literal_leading_dot: Option, +} + +impl Opener { + /// Open a url with a default or specific program. + /// + /// # Examples + /// + /// ```rust,no_run + /// use tauri_plugin_opener::OpenerExt; + /// + /// tauri::Builder::default() + /// .setup(|app| { + /// // open the given URL on the system default browser + /// app.opener().open_url("https://github.com/tauri-apps/tauri", None::<&str>)?; + /// Ok(()) + /// }); + /// ``` + /// + /// ## Platform-specific: + /// + /// - **Android / iOS**: Always opens using default program, unless `with` is provided as "inAppBrowser". + #[cfg(desktop)] + pub fn open_url(&self, url: impl Into, with: Option>) -> Result<()> { + crate::open::open( + url.into(), + with.map(Into::into).filter(|with| with != "inAppBrowser"), + ) + } + + /// Open a url with a default or specific program. + /// + /// # Examples + /// + /// ```rust,no_run + /// use tauri_plugin_opener::OpenerExt; + /// + /// tauri::Builder::default() + /// .setup(|app| { + /// // open the given URL on the system default browser + /// app.opener().open_url("https://github.com/tauri-apps/tauri", None::<&str>)?; + /// Ok(()) + /// }); + /// ``` + /// + /// ## Platform-specific: + /// + /// - **Android / iOS**: Always opens using default program, unless `with` is provided as "inAppBrowser". + #[cfg(mobile)] + pub fn open_url(&self, url: impl Into, with: Option>) -> Result<()> { + self.mobile_plugin_handle + .run_mobile_plugin( + "open", + serde_json::json!({ "url": url.into(), "with": with.map(Into::into) }), + ) + .map_err(Into::into) + } + + /// Open a path with a default or specific program. + /// + /// # Examples + /// + /// ```rust,no_run + /// use tauri_plugin_opener::OpenerExt; + /// + /// tauri::Builder::default() + /// .setup(|app| { + /// // open the given path on the system default explorer + /// app.opener().open_path("/path/to/file", None::<&str>)?; + /// Ok(()) + /// }); + /// ``` + /// + /// ## Platform-specific: + /// + /// - **Android / iOS**: Always opens using default program. + #[cfg(desktop)] + pub fn open_path( + &self, + path: impl Into, + with: Option>, + ) -> Result<()> { + crate::open::open( + path.into(), + with.map(Into::into).filter(|with| with != "inAppBrowser"), + ) + } + + /// Open a path with a default or specific program. + /// + /// # Examples + /// + /// ```rust,no_run + /// use tauri_plugin_opener::OpenerExt; + /// + /// tauri::Builder::default() + /// .setup(|app| { + /// // open the given path on the system default explorer + /// app.opener().open_path("/path/to/file", None::<&str>)?; + /// Ok(()) + /// }); + /// ``` + /// + /// ## Platform-specific: + /// + /// - **Android / iOS**: Always opens using default program. + #[cfg(mobile)] + pub fn open_path( + &self, + path: impl Into, + _with: Option>, + ) -> Result<()> { + self.mobile_plugin_handle + .run_mobile_plugin("open", path.into()) + .map_err(Into::into) + } + + /// Reveal a path in the system's default explorer. See [`reveal_item_in_dir`] for details. + /// + /// ## Platform-specific: + /// + /// - **Android / iOS:** Unsupported, returns [`Error::UnsupportedPlatform`]. + pub fn reveal_item_in_dir>(&self, p: P) -> Result<()> { + reveal_item_in_dir(p) + } + + /// Reveal multiple paths in the system's default explorer. See [`reveal_items_in_dir`] for details. + /// + /// ## Platform-specific: + /// + /// - **Android / iOS:** Unsupported, returns [`Error::UnsupportedPlatform`]. + pub fn reveal_items_in_dir(&self, paths: I) -> Result<()> + where + I: IntoIterator, + P: AsRef, + { + reveal_items_in_dir(paths) + } +} + +/// Extensions to [`tauri::App`], [`tauri::AppHandle`], [`tauri::WebviewWindow`], [`tauri::Webview`] and [`tauri::Window`] to access the opener APIs. +pub trait OpenerExt { + /// Returns the [`Opener`] instance managed by the plugin. + fn opener(&self) -> &Opener; +} + +impl> OpenerExt for T { + fn opener(&self) -> &Opener { + self.state::>().inner() + } +} + +/// The opener plugin Builder. +pub struct Builder { + open_js_links_on_click: bool, +} + +impl Default for Builder { + fn default() -> Self { + Self { + open_js_links_on_click: true, + } + } +} + +impl Builder { + /// Create a new opener plugin Builder. + pub fn new() -> Self { + Self::default() + } + + /// Whether the plugin should inject a JS script to open URLs in default browser + /// when clicking on `` elements that has `_blank` target, or when pressing `Ctrl` or `Shift` while clicking it. + /// + /// Enabled by default for `http:`, `https:`, `mailto:`, `tel:` links. + pub fn open_js_links_on_click(mut self, open: bool) -> Self { + self.open_js_links_on_click = open; + self + } + + /// Build and Initializes the plugin. + pub fn build(self) -> TauriPlugin> { + let mut builder = tauri::plugin::Builder::>::new("opener") + .setup(|app, api| { + #[cfg(target_os = "android")] + let handle = api.register_android_plugin(PLUGIN_IDENTIFIER, "OpenerPlugin")?; + #[cfg(target_os = "ios")] + let handle = api.register_ios_plugin(init_plugin_opener)?; + + app.manage(Opener { + #[cfg(not(mobile))] + _marker: std::marker::PhantomData:: R>, + #[cfg(mobile)] + mobile_plugin_handle: handle, + require_literal_leading_dot: api + .config() + .as_ref() + .and_then(|c| c.require_literal_leading_dot), + }); + Ok(()) + }) + .invoke_handler(tauri::generate_handler![ + commands::open_url, + commands::open_path, + commands::reveal_item_in_dir, + ]); + + if self.open_js_links_on_click { + builder = builder.js_init_script(include_str!("init-iife.js").to_string()); + } + + builder.build() + } +} + +/// Initializes the plugin. +pub fn init() -> TauriPlugin> { + Builder::default().build() +} diff --git a/plugins/opener/src/open.rs b/plugins/opener/src/open.rs new file mode 100644 index 0000000..a3d46c5 --- /dev/null +++ b/plugins/opener/src/open.rs @@ -0,0 +1,61 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +//! Types and functions related to shell. + +use std::{ffi::OsStr, path::Path}; + +pub(crate) fn open, S: AsRef>(path: P, with: Option) -> crate::Result<()> { + match with { + Some(program) => ::open::with_detached(path, program.as_ref()), + None => ::open::that_detached(path), + } + .map_err(Into::into) +} + +/// Opens URL with the program specified in `with`, or system default if `None`. +/// +/// ## Platform-specific: +/// +/// - **Android / iOS**: Always opens using default program. +/// +/// # Examples +/// +/// ```rust,no_run +/// tauri::Builder::default() +/// .setup(|app| { +/// // open the given URL on the system default browser +/// tauri_plugin_opener::open_url("https://github.com/tauri-apps/tauri", None::<&str>)?; +/// Ok(()) +/// }); +/// ``` +pub fn open_url, S: AsRef>(url: P, with: Option) -> crate::Result<()> { + let url = url.as_ref(); + open(url, with) +} + +/// Opens path with the program specified in `with`, or system default if `None`. +/// +/// ## Platform-specific: +/// +/// - **Android / iOS**: Always opens using default program. +/// +/// # Examples +/// +/// ```rust,no_run +/// tauri::Builder::default() +/// .setup(|app| { +/// // open the given URL on the system default explorer +/// tauri_plugin_opener::open_path("/path/to/file", None::<&str>)?; +/// Ok(()) +/// }); +/// ``` +pub fn open_path, S: AsRef>(path: P, with: Option) -> crate::Result<()> { + let path = path.as_ref(); + if with.is_none() { + // Returns an IO error if not exists, and besides `exists()` is a shorthand for `metadata()` + _ = path.metadata()?; + } + open(path, with) +} diff --git a/plugins/opener/src/reveal_item_in_dir.rs b/plugins/opener/src/reveal_item_in_dir.rs new file mode 100644 index 0000000..9578a28 --- /dev/null +++ b/plugins/opener/src/reveal_item_in_dir.rs @@ -0,0 +1,311 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use std::path::{Path, PathBuf}; + +/// Reveal a path in the system's default explorer. +/// +/// ## Platform-specific: +/// +/// - **Android / iOS:** Unsupported. +pub fn reveal_item_in_dir>(path: P) -> crate::Result<()> { + let path = canonicalize(path.as_ref())?; + + #[cfg(any( + windows, + target_os = "macos", + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd" + ))] + return imp::reveal_items_in_dir(&[path]); + + #[cfg(not(any( + windows, + target_os = "macos", + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd" + )))] + Err(crate::Error::UnsupportedPlatform) +} + +/// Reveal multiple paths in the system's default explorer. +/// +/// ## Platform-specific: +/// +/// - **Android / iOS:** Unsupported. +pub fn reveal_items_in_dir(paths: I) -> crate::Result<()> +where + I: IntoIterator, + P: AsRef, +{ + let mut canonicalized = vec![]; + + for path in paths { + let path = canonicalize(path.as_ref())?; + canonicalized.push(path); + } + + #[cfg(any( + windows, + target_os = "macos", + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd" + ))] + return imp::reveal_items_in_dir(&canonicalized); + + #[cfg(not(any( + windows, + target_os = "macos", + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd" + )))] + Err(crate::Error::UnsupportedPlatform) +} + +fn canonicalize(path: &Path) -> crate::Result { + #[cfg(windows)] + let path = crate::windows_shell_path::absolute_and_check_exists(dunce::simplified(path))?; + #[cfg(not(windows))] + let path = std::fs::canonicalize(path)?; + Ok(path) +} + +#[cfg(windows)] +mod imp { + use std::{ + borrow::Cow, + collections::HashMap, + path::{Path, PathBuf}, + }; + + use windows::Win32::UI::Shell::Common::ITEMIDLIST; + use windows::{ + core::{w, HSTRING, PCWSTR}, + Win32::{ + Foundation::ERROR_FILE_NOT_FOUND, + System::Com::CoInitialize, + UI::{ + Shell::{ + ILCreateFromPathW, ILFree, SHOpenFolderAndSelectItems, ShellExecuteExW, + SHELLEXECUTEINFOW, + }, + WindowsAndMessaging::SW_SHOWNORMAL, + }, + }, + }; + + pub fn reveal_items_in_dir(paths: &[PathBuf]) -> crate::Result<()> { + if paths.is_empty() { + return Ok(()); + } + + let mut grouped_paths: HashMap, Vec<&Path>> = HashMap::new(); + for path in paths { + let parent = crate::windows_shell_path::shell_parent_path(path) + .ok_or_else(|| crate::Error::NoParent(path.to_path_buf()))?; + grouped_paths.entry(parent).or_default().push(path); + } + + let _ = unsafe { CoInitialize(None) }; + + for (parent, to_reveals) in grouped_paths { + let parent_item_id_list = OwnedItemIdList::new(&parent)?; + let to_reveals_item_id_list = to_reveals + .iter() + .map(|to_reveal| OwnedItemIdList::new(to_reveal)) + .collect::>>()?; + if let Err(e) = unsafe { + SHOpenFolderAndSelectItems( + parent_item_id_list.item, + Some( + &to_reveals_item_id_list + .iter() + .map(|item| item.item) + .collect::>(), + ), + 0, + ) + } { + // from https://github.com/electron/electron/blob/10d967028af2e72382d16b7e2025d243b9e204ae/shell/common/platform_util_win.cc#L302 + // On some systems, the above call mysteriously fails with "file not + // found" even though the file is there. In these cases, ShellExecute() + // seems to work as a fallback (although it won't select the file). + // + // Note: we only handle the first file here if multiple of are present + if e.code().0 == ERROR_FILE_NOT_FOUND.0 as i32 { + let first_path = to_reveals[0]; + let is_dir = first_path.is_dir(); + let mut info = SHELLEXECUTEINFOW { + cbSize: std::mem::size_of::() as _, + nShow: SW_SHOWNORMAL.0, + lpFile: PCWSTR(parent_item_id_list.hstring.as_ptr()), + lpClass: if is_dir { w!("folder") } else { PCWSTR::null() }, + lpVerb: if is_dir { + w!("explore") + } else { + PCWSTR::null() + }, + ..Default::default() + }; + + unsafe { ShellExecuteExW(&mut info) }?; + } + } + } + + Ok(()) + } + + struct OwnedItemIdList { + hstring: HSTRING, + item: *const ITEMIDLIST, + } + + impl OwnedItemIdList { + fn new(path: &Path) -> crate::Result { + let path_hstring = HSTRING::from(path); + let item_id_list = unsafe { ILCreateFromPathW(&path_hstring) }; + if item_id_list.is_null() { + Err(crate::Error::FailedToConvertPathToItemIdList( + path.to_owned(), + )) + } else { + Ok(Self { + hstring: path_hstring, + item: item_id_list, + }) + } + } + } + + impl Drop for OwnedItemIdList { + fn drop(&mut self) { + if !self.item.is_null() { + unsafe { ILFree(Some(self.item)) }; + } + } + } +} + +#[cfg(any( + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd" +))] +mod imp { + use super::*; + use std::collections::HashMap; + use std::path::PathBuf; + + pub fn reveal_items_in_dir(paths: &[PathBuf]) -> crate::Result<()> { + let connection = zbus::blocking::Connection::session()?; + + reveal_with_filemanager1(paths, &connection).or_else(|e| { + // Fallback to opening the directory of the first item if revealing multiple items fails. + if let Some(first_path) = paths.first() { + reveal_with_open_uri_portal(first_path, &connection) + } else { + Err(e) + } + }) + } + + fn reveal_with_filemanager1( + paths: &[PathBuf], + connection: &zbus::blocking::Connection, + ) -> crate::Result<()> { + let uris: Result, _> = paths + .iter() + .map(|path| { + url::Url::from_file_path(path) + .map_err(|_| crate::Error::FailedToConvertPathToFileUrl) + }) + .collect(); + let uris = uris?; + let uri_strs: Vec<&str> = uris.iter().map(|uri| uri.as_str()).collect(); + + #[zbus::proxy( + interface = "org.freedesktop.FileManager1", + default_service = "org.freedesktop.FileManager1", + default_path = "/org/freedesktop/FileManager1" + )] + trait FileManager1 { + async fn ShowItems(&self, name: Vec<&str>, arg2: &str) -> crate::Result<()>; + } + + let proxy = FileManager1ProxyBlocking::new(connection)?; + + proxy.ShowItems(uri_strs, "") + } + + fn reveal_with_open_uri_portal( + path: &Path, + connection: &zbus::blocking::Connection, + ) -> crate::Result<()> { + let uri = url::Url::from_file_path(path) + .map_err(|_| crate::Error::FailedToConvertPathToFileUrl)?; + + #[zbus::proxy( + interface = "org.freedesktop.portal.Desktop", + default_service = "org.freedesktop.portal.OpenURI", + default_path = "/org/freedesktop/portal/desktop" + )] + trait PortalDesktop { + async fn OpenDirectory( + &self, + arg1: &str, + name: &str, + arg3: HashMap<&str, &str>, + ) -> crate::Result<()>; + } + + let proxy = PortalDesktopProxyBlocking::new(connection)?; + + proxy.OpenDirectory("", uri.as_str(), HashMap::new()) + } +} + +#[cfg(target_os = "macos")] +mod imp { + use objc2_app_kit::NSWorkspace; + use objc2_foundation::{NSArray, NSString, NSURL}; + use std::path::PathBuf; + + pub fn reveal_items_in_dir(paths: &[PathBuf]) -> crate::Result<()> { + // these AppKit calls are only `unsafe` before objc2-app-kit 0.3.2 + #[allow(unused_unsafe)] + unsafe { + let mut urls = Vec::new(); + + for path in paths { + let path = path.to_string_lossy(); + let path = NSString::from_str(&path); + let url = NSURL::fileURLWithPath(&path); + + urls.push(url); + } + + let urls = NSArray::from_retained_slice(&urls); + + let workspace = NSWorkspace::new(); + workspace.activateFileViewerSelectingURLs(&urls); + } + + Ok(()) + } +} diff --git a/plugins/opener/src/scope.rs b/plugins/opener/src/scope.rs new file mode 100644 index 0000000..275913d --- /dev/null +++ b/plugins/opener/src/scope.rs @@ -0,0 +1,141 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use std::{ + marker::PhantomData, + path::{Path, PathBuf}, + sync::Arc, +}; + +use tauri::{ipc::ScopeObject, utils::acl::Value, AppHandle, Manager, Runtime}; + +use crate::{scope_entry::EntryRaw, Error}; + +pub use crate::scope_entry::Application; + +#[derive(Debug)] +pub enum Entry { + Url { + url: glob::Pattern, + app: Application, + }, + Path { + path: Option, + app: Application, + }, +} + +impl ScopeObject for Entry { + type Error = Error; + + fn deserialize( + app_handle: &AppHandle, + raw: Value, + ) -> std::result::Result { + serde_json::from_value(raw.into()) + .and_then(|raw| { + let entry = match raw { + EntryRaw::Url { url, app } => Entry::Url { + url: glob::Pattern::new(&url) + .map_err(|e| serde::de::Error::custom(e.to_string()))?, + app, + }, + EntryRaw::Path { path, app } => { + let path = match app_handle.path().parse(path) { + Ok(path) => Some(path), + #[cfg(not(target_os = "android"))] + Err(tauri::Error::UnknownPath) => None, + Err(err) => return Err(serde::de::Error::custom(err.to_string())), + }; + + Entry::Path { path, app } + } + }; + + Ok(entry) + }) + .map_err(Into::into) + } +} + +impl Application { + fn matches(&self, a: Option<&str>) -> bool { + match self { + Self::Default => a.is_none(), + Self::Enable(enable) => *enable, + Self::App(program) => Some(program.as_str()) == a, + } + } +} + +impl Entry { + fn path(&self) -> Option { + match self { + Self::Url { .. } => None, + Self::Path { path, .. } => path.clone(), + } + } + + fn matches_url(&self, u: &str, a: Option<&str>) -> bool { + match self { + Self::Url { url, app } => url.matches(u) && app.matches(a), + Self::Path { .. } => false, + } + } + + fn matches_path_program(&self, a: Option<&str>) -> bool { + match self { + Self::Url { .. } => false, + Self::Path { app, .. } => app.matches(a), + } + } +} + +#[derive(Debug)] +pub struct Scope<'a, R: Runtime, M: Manager> { + allowed: Vec<&'a Arc>, + denied: Vec<&'a Arc>, + manager: &'a M, + _marker: PhantomData, +} + +impl<'a, R: Runtime, M: Manager> Scope<'a, R, M> { + pub(crate) fn new( + manager: &'a M, + allowed: Vec<&'a Arc>, + denied: Vec<&'a Arc>, + ) -> Self { + Self { + manager, + allowed, + denied, + _marker: PhantomData, + } + } + + pub fn is_url_allowed(&self, url: &str, with: Option<&str>) -> bool { + let denied = self.denied.iter().any(|e| e.matches_url(url, with)); + if denied { + false + } else { + self.allowed.iter().any(|e| e.matches_url(url, with)) + } + } + + pub fn is_path_allowed(&self, path: &Path, with: Option<&str>) -> crate::Result { + let fs_scope = tauri::fs::Scope::new( + self.manager, + &tauri::utils::config::FsScope::Scope { + allow: self.allowed.iter().filter_map(|e| e.path()).collect(), + deny: self.denied.iter().filter_map(|e| e.path()).collect(), + require_literal_leading_dot: self + .manager + .state::>() + .require_literal_leading_dot, + }, + )?; + + Ok(fs_scope.is_allowed(path) && self.allowed.iter().any(|e| e.matches_path_program(with))) + } +} diff --git a/plugins/opener/src/scope_entry.rs b/plugins/opener/src/scope_entry.rs new file mode 100644 index 0000000..d4867b4 --- /dev/null +++ b/plugins/opener/src/scope_entry.rs @@ -0,0 +1,32 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use std::path::PathBuf; + +use serde::Deserialize; + +#[derive(Deserialize, Debug)] +#[serde(untagged)] +#[derive(Default)] +pub enum Application { + #[default] + Default, + Enable(bool), + App(String), +} + +#[derive(Deserialize)] +#[serde(untagged, rename_all = "camelCase")] +pub(crate) enum EntryRaw { + Url { + url: String, + #[serde(default)] + app: Application, + }, + Path { + path: PathBuf, + #[serde(default)] + app: Application, + }, +} diff --git a/plugins/opener/src/windows_shell_path.rs b/plugins/opener/src/windows_shell_path.rs new file mode 100644 index 0000000..8715641 --- /dev/null +++ b/plugins/opener/src/windows_shell_path.rs @@ -0,0 +1,255 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use std::{ + borrow::Cow, + ffi::OsString, + io, + os::windows::ffi::OsStringExt, + path::{Component, Path, PathBuf, Prefix, PrefixComponent}, +}; + +use windows::{core::HSTRING, Win32::Storage::FileSystem::GetFullPathNameW}; + +pub fn absolute_and_check_exists(path: &Path) -> io::Result { + let path = absolute(path)?; + if path.exists() { + Ok(path) + } else { + Err(std::io::Error::new( + std::io::ErrorKind::NotFound, + "path doesn't exist", + )) + } +} + +// TODO: Switch to use `std::path::absolute` once MSRV > 1.79 +// Modified from https://github.com/rust-lang/rust/blob/b49ecc9eb70a51e89f32a7358e790f7b3808ccb3/library/std/src/sys/path/windows.rs#L185 +// Note: this doesn't resolve symlinks +fn absolute(path: &Path) -> io::Result { + if path.as_os_str().is_empty() { + return Err(io::Error::new( + io::ErrorKind::InvalidInput, + "cannot make an empty path absolute", + )); + } + + let prefix = path.components().next(); + // Verbatim paths should not be modified. + if prefix + .map(|component| { + let Component::Prefix(prefix) = component else { + return false; + }; + matches!( + prefix.kind(), + Prefix::Verbatim(..) | Prefix::VerbatimDisk(..) | Prefix::VerbatimUNC(..) + ) + }) + .unwrap_or(false) + { + // NULs in verbatim paths are rejected for consistency. + if path.as_os_str().as_encoded_bytes().contains(&0) { + return Err(io::Error::new( + io::ErrorKind::InvalidInput, + "strings passed to WinAPI cannot contain NULs", + )); + } + return Ok(path.to_owned()); + } + + // This is an additional check to make sure we don't pass in a single driver letter to GetFullPathNameW + // which will resolves to the current working directory + // + // > https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-getfullpathnamew#:~:text=If%20you%20specify%20%22U%3A%22%20the%20path%20returned%20is%20the%20current%20directory%20on%20the%20%22U%3A%5C%22%20drive + #[allow(clippy::collapsible_if)] + if let Some(Component::Prefix(last_prefix)) = path.components().next_back() { + if matches!(last_prefix.kind(), Prefix::Disk(..)) { + return Ok(PathBuf::from(last_prefix.as_os_str())); + } + } + + let path_hstring = HSTRING::from(path); + + let size = unsafe { GetFullPathNameW(&path_hstring, None, None) }; + if size == 0 { + return Err(io::Error::last_os_error()); + } + let mut buffer = vec![0; size as usize]; + let size = unsafe { GetFullPathNameW(&path_hstring, Some(&mut buffer), None) }; + if size == 0 { + return Err(io::Error::last_os_error()); + } + + Ok(PathBuf::from(OsString::from_wide(&buffer[..size as usize]))) +} + +/// Similar to [`Path::parent`] but resolves parent of `C:`/`C:\` to `""` and handles UNC host name (`\\wsl.localhost\Ubuntu\` to `\\wsl.localhost`) +pub fn shell_parent_path(path: &Path) -> Option> { + fn handle_prefix(prefix: PrefixComponent<'_>) -> Option> { + match prefix.kind() { + Prefix::UNC(host_name, _share_name) => { + let mut path = OsString::from(r"\\"); + path.push(host_name); + Some(PathBuf::from(path).into()) + } + Prefix::Disk(_) => Some(PathBuf::from("").into()), + _ => None, + } + } + + let mut components = path.components(); + let component = components.next_back()?; + match component { + Component::Normal(_) | Component::CurDir | Component::ParentDir => { + Some(components.as_path().into()) + } + Component::Prefix(prefix) => handle_prefix(prefix), + // Handle cases like `C:\` and `\\wsl.localhost\Ubuntu\` + Component::RootDir => { + if let Component::Prefix(prefix) = components.next_back()? { + handle_prefix(prefix) + } else { + None + } + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use std::path::Path; + + // absolute() tests + + #[test] + fn absolute_empty_error() { + let err = absolute(Path::new("")).unwrap_err(); + assert_eq!(err.kind(), io::ErrorKind::InvalidInput); + } + + #[test] + fn absolute_verbatim_passthrough() { + let path = Path::new(r"\\?\C:\foo"); + assert_eq!(absolute(path).unwrap(), path); + } + + #[test] + fn absolute_verbatim_unc_passthrough() { + let path = Path::new(r"\\?\UNC\server\share"); + assert_eq!(absolute(path).unwrap(), path); + } + + #[test] + fn absolute_bare_drive_letter() { + let result = absolute(Path::new("C:")).unwrap(); + assert_eq!(result, Path::new("C:")); + } + + #[test] + fn absolute_already_absolute() { + let result = absolute(Path::new(r"C:\Windows")).unwrap(); + assert_eq!(result, Path::new(r"C:\Windows")); + } + + #[test] + fn absolute_unc_path() { + let result = absolute(Path::new(r"\\server\share\folder")).unwrap(); + assert_eq!(result, Path::new(r"\\server\share\folder")); + } + + #[test] + fn absolute_converts_forward_slashes() { + let result = absolute(Path::new("C:/Windows/System32")).unwrap(); + assert_eq!(result, Path::new(r"C:\Windows\System32")); + } + + // absolute_and_check_exists() tests + + #[test] + fn absolute_and_check_exists_existing_path() { + assert!(absolute_and_check_exists(Path::new(r"C:\Windows")).is_ok()); + } + + #[test] + fn absolute_and_check_exists_nonexistent_path() { + let err = absolute_and_check_exists(Path::new(r"C:\nonexistent_xyz_12345")).unwrap_err(); + assert_eq!(err.kind(), io::ErrorKind::NotFound); + } + + #[test] + fn absolute_and_check_exists_empty_propagates() { + let err = absolute_and_check_exists(Path::new("")).unwrap_err(); + assert_eq!(err.kind(), io::ErrorKind::InvalidInput); + } + + // shell_parent_path() tests + + #[test] + fn shell_parent_path_local_path() { + let result = shell_parent_path(Path::new(r"C:\Users\foo")); + assert_eq!(result.as_deref(), Some(Path::new(r"C:\Users"))); + } + + #[test] + fn shell_parent_path_nested_path() { + let result = shell_parent_path(Path::new(r"C:\a\b\c\d")); + assert_eq!(result.as_deref(), Some(Path::new(r"C:\a\b\c"))); + } + + #[test] + fn shell_parent_path_drive_root_trailing() { + let result = shell_parent_path(Path::new(r"C:\")); + assert_eq!(result.as_deref(), Some(Path::new(""))); + } + + #[test] + fn shell_parent_path_bare_drive() { + let result = shell_parent_path(Path::new("C:")); + assert_eq!(result.as_deref(), Some(Path::new(""))); + } + + #[test] + fn shell_parent_path_unc_with_subfolder() { + let result = shell_parent_path(Path::new(r"\\server\share\folder")); + assert_eq!(result.as_deref(), Some(Path::new(r"\\server\share"))); + } + + #[test] + fn shell_parent_path_unc_share_trailing_slash() { + let result = shell_parent_path(Path::new(r"\\server.local\share\")); + assert_eq!(result.as_deref(), Some(Path::new(r"\\server.local"))); + } + + #[test] + fn shell_parent_path_unc_share_no_slash() { + let result = shell_parent_path(Path::new(r"\\server\share")); + assert_eq!(result.as_deref(), Some(Path::new(r"\\server"))); + } + + #[test] + fn shell_parent_path_relative() { + let result = shell_parent_path(Path::new(r"foo\bar")); + assert_eq!(result.as_deref(), Some(Path::new("foo"))); + } + + #[test] + fn shell_parent_path_single_component() { + let result = shell_parent_path(Path::new("foo")); + assert_eq!(result.as_deref(), Some(Path::new(""))); + } + + #[test] + fn shell_parent_path_empty() { + let result = shell_parent_path(Path::new("")); + assert!(result.is_none()); + } + + #[test] + fn shell_parent_path_verbatim() { + let result = shell_parent_path(Path::new(r"\\?\C:\foo")); + assert_eq!(result.as_deref(), Some(Path::new(r"\\?\C:\"))); + } +} diff --git a/plugins/opener/tsconfig.json b/plugins/opener/tsconfig.json new file mode 100644 index 0000000..4374468 --- /dev/null +++ b/plugins/opener/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "rootDir": "guest-js" + }, + "include": ["guest-js/*.ts"] +} diff --git a/plugins/os/CHANGELOG.md b/plugins/os/CHANGELOG.md new file mode 100644 index 0000000..603b243 --- /dev/null +++ b/plugins/os/CHANGELOG.md @@ -0,0 +1,124 @@ +# Changelog + +## \[2.3.2] + +- [`93426f85`](https://github.com/tauri-apps/plugins-workspace/commit/93426f85120f49beb9f40222bff45185a32d54a9) Fixed an issue that caused docs.rs builds to fail. No user facing changes. + +## \[2.3.1] + +- [`d3d290ab`](https://github.com/tauri-apps/plugins-workspace/commit/d3d290ab8a8913981a98e2eb7f2c5d4aba3bc36c) ([#2912](https://github.com/tauri-apps/plugins-workspace/pull/2912) by [@FabianLars](https://github.com/tauri-apps/plugins-workspace/../../FabianLars)) Unlocked version of `serialize-to-javascript` from `=0.1.1` to `^0.1.1` for compatibility with Tauri's upcoming version `2.8`. + +## \[2.3.0] + +- [`f209b2f2`](https://github.com/tauri-apps/plugins-workspace/commit/f209b2f23cb29133c97ad5961fb46ef794dbe063) ([#2804](https://github.com/tauri-apps/plugins-workspace/pull/2804) by [@renovate](https://github.com/tauri-apps/plugins-workspace/../../renovate)) Updated tauri to 2.6 + +## \[2.2.2] + +- [`f634e524`](https://github.com/tauri-apps/plugins-workspace/commit/f634e5248ebe428f8305a59f74c13fc15147fb8e) This is an "empty" release to update the plugins' source files on crates.io and docs.rs. This should fix docs.rs build failures for projects using tauri plugins as dependencies. + +## \[2.2.1] + +- [`a1b3fa27`](https://github.com/tauri-apps/plugins-workspace/commit/a1b3fa27f11022c9b6622b4fab12d93239eb05de) ([#2515](https://github.com/tauri-apps/plugins-workspace/pull/2515) by [@FabianLars](https://github.com/tauri-apps/plugins-workspace/../../FabianLars)) Re-exported the `Geolocation`, `Haptics`, `Notification`, and `Os` structs so that they show up on docs.rs. + +## \[2.2.0] + +- [`3a79266b`](https://github.com/tauri-apps/plugins-workspace/commit/3a79266b8cf96a55b1ae6339d725567d45a44b1d) ([#2173](https://github.com/tauri-apps/plugins-workspace/pull/2173) by [@FabianLars](https://github.com/tauri-apps/plugins-workspace/../../FabianLars)) Bumped all plugins to `v2.2.0`. From now, the versions for the Rust and JavaScript packages of each plugin will be in sync with each other. + +## \[2.0.1] + +- [`a1a82208`](https://github.com/tauri-apps/plugins-workspace/commit/a1a82208ed4ab87f83310be0dc95428aec9ab241) ([#1873](https://github.com/tauri-apps/plugins-workspace/pull/1873) by [@lucasfernog](https://github.com/tauri-apps/plugins-workspace/../../lucasfernog)) Downgrade MSRV to 1.77.2 to support Windows 7. + +## \[2.0.0] + +- [`e2c4dfb6`](https://github.com/tauri-apps/plugins-workspace/commit/e2c4dfb6af43e5dd8d9ceba232c315f5febd55c1) Update to tauri v2 stable release. + +## \[2.0.0-rc.1] + +- [`e2e97db5`](https://github.com/tauri-apps/plugins-workspace/commit/e2e97db51983267f5be84d4f6f0278d58834d1f5) ([#1701](https://github.com/tauri-apps/plugins-workspace/pull/1701) by [@lucasfernog](https://github.com/tauri-apps/plugins-workspace/../../lucasfernog)) Update to tauri 2.0.0-rc.8 + +## \[2.0.0-rc.0] + +- [`9887d1`](https://github.com/tauri-apps/plugins-workspace/commit/9887d14bd0e971c4c0f5c1188fc4005d3fc2e29e) Update to tauri RC. + +## \[2.0.0-beta.8] + +- [`99d6ac0f`](https://github.com/tauri-apps/plugins-workspace/commit/99d6ac0f9506a6a4a1aa59c728157190a7441af6) ([#1606](https://github.com/tauri-apps/plugins-workspace/pull/1606) by [@FabianLars](https://github.com/tauri-apps/plugins-workspace/../../FabianLars)) The JS packages now specify the *minimum* `@tauri-apps/api` version instead of a single exact version. +- [`6de87966`](https://github.com/tauri-apps/plugins-workspace/commit/6de87966ecc00ad9d91c25be452f1f46bd2b7e1f) ([#1597](https://github.com/tauri-apps/plugins-workspace/pull/1597) by [@Legend-Master](https://github.com/tauri-apps/plugins-workspace/../../Legend-Master)) Update to tauri beta.25. + +## \[2.0.0-beta.7] + +- [`40ef9a81`](https://github.com/tauri-apps/plugins-workspace/commit/40ef9a818fb03457819c1d72ea84de57fbf868ba) ([#1514](https://github.com/tauri-apps/plugins-workspace/pull/1514) by [@fynntang](https://github.com/tauri-apps/plugins-workspace/../../fynntang)) **Changed:** `platform`, `arch`, `type`, `family`, `version` and `exe_extension` functions in the documentation examples to better reflect that these functions are synchronous. +- [`22a17980`](https://github.com/tauri-apps/plugins-workspace/commit/22a17980ff4f6f8c40adb1b8f4ffc6dae2fe7e30) ([#1537](https://github.com/tauri-apps/plugins-workspace/pull/1537) by [@lucasfernog](https://github.com/tauri-apps/plugins-workspace/../../lucasfernog)) Update to tauri beta.24. + +## \[2.0.0-beta.6] + +- [`0959fe37`](https://github.com/tauri-apps/plugins-workspace/commit/0959fe3757250c6dea6247edb20e6ab468f20511) ([#1353](https://github.com/tauri-apps/plugins-workspace/pull/1353) by [@amrbashir](https://github.com/tauri-apps/plugins-workspace/../../amrbashir)) **Breaking** Changed `platform`, `arch`, `type`, `family`, `version` and `exe_extension` functions to be sync. +- [`76daee7a`](https://github.com/tauri-apps/plugins-workspace/commit/76daee7aafece34de3092c86e531cf9eb1138989) ([#1512](https://github.com/tauri-apps/plugins-workspace/pull/1512) by [@renovate](https://github.com/tauri-apps/plugins-workspace/../../renovate)) Update to tauri beta.23. + +## \[2.0.0-beta.5] + +- [`9013854f`](https://github.com/tauri-apps/plugins-workspace/commit/9013854f42a49a230b9dbb9d02774765528a923f)([#1382](https://github.com/tauri-apps/plugins-workspace/pull/1382)) Update to tauri beta.22. + +## \[2.0.0-beta.4] + +- [`430bd6f4`](https://github.com/tauri-apps/plugins-workspace/commit/430bd6f4f379bee5d232ae6b098ae131db7f178a)([#1363](https://github.com/tauri-apps/plugins-workspace/pull/1363)) Update to tauri beta.20. + +## \[2.0.0-beta.3] + +- [`bd1ed590`](https://github.com/tauri-apps/plugins-workspace/commit/bd1ed5903ffcce5500310dac1e59e8c67674ef1e)([#1237](https://github.com/tauri-apps/plugins-workspace/pull/1237)) Update to tauri beta.17. + +## \[2.0.0-beta.3] + +- [`a04ea2f`](https://github.com/tauri-apps/plugins-workspace/commit/a04ea2f38294d5a3987578283badc8eec87a7752)([#1071](https://github.com/tauri-apps/plugins-workspace/pull/1071)) The global API script is now only added to the binary when the `withGlobalTauri` config is true. + +## \[2.0.0-beta.2] + +- [`99bea25`](https://github.com/tauri-apps/plugins-workspace/commit/99bea2559c2c0648c2519c50a18cd124dacef57b)([#1005](https://github.com/tauri-apps/plugins-workspace/pull/1005)) Update to tauri beta.8. + +## \[2.0.0-beta.1] + +- [`569defb`](https://github.com/tauri-apps/plugins-workspace/commit/569defbe9492e38938554bb7bdc1be9151456d21) Update to tauri beta.4. + +## \[2.0.0-beta.0] + +- [`d198c01`](https://github.com/tauri-apps/plugins-workspace/commit/d198c014863ee260cb0de88a14b7fc4356ef7474)([#862](https://github.com/tauri-apps/plugins-workspace/pull/862)) Update to tauri beta. + +## \[2.0.0-alpha.6] + +- [`387c2f9`](https://github.com/tauri-apps/plugins-workspace/commit/387c2f9e0ce4c75c07ffa3fd76391a25b58f5daf)([#802](https://github.com/tauri-apps/plugins-workspace/pull/802)) Update to @tauri-apps/api v2.0.0-alpha.13. + +## \[2.0.0-alpha.5] + +- [`387c2f9`](https://github.com/tauri-apps/plugins-workspace/commit/387c2f9e0ce4c75c07ffa3fd76391a25b58f5daf)([#802](https://github.com/tauri-apps/plugins-workspace/pull/802)) Update to @tauri-apps/api v2.0.0-alpha.12. +- [`fc62ead`](https://github.com/tauri-apps/plugins-workspace/commit/fc62ead56515b64138b8342af1c5ec6071b715fc)([#721](https://github.com/tauri-apps/plugins-workspace/pull/721)) Fix `Uncaught TypeError: Cannot read properties of undefined (reading 'os')` + +## \[2.0.0-alpha.4] + +- [`e438e0a`](https://github.com/tauri-apps/plugins-workspace/commit/e438e0a62d4b430a5159f05f13ecd397dd891a0d)([#676](https://github.com/tauri-apps/plugins-workspace/pull/676)) Update to @tauri-apps/api v2.0.0-alpha.11. + +## \[2.0.0-alpha.3] + +- [`5c13736`](https://github.com/tauri-apps/plugins-workspace/commit/5c137365c60790e8d4037d449e8237aa3fffdab0)([#673](https://github.com/tauri-apps/plugins-workspace/pull/673)) Update to @tauri-apps/api v2.0.0-alpha.9. + +## \[2.0.0-alpha.2] + +- [`4e2cef9`](https://github.com/tauri-apps/plugins-workspace/commit/4e2cef9b702bbbb9cf4ee17de50791cb21f1b2a4)([#593](https://github.com/tauri-apps/plugins-workspace/pull/593)) Update to alpha.12. + +## \[2.0.0-alpha.2] + +- [`e510f2f`](https://github.com/tauri-apps/plugins-workspace/commit/e510f2fe4c227c107a1faca9386b5ceb326611ed)([#561](https://github.com/tauri-apps/plugins-workspace/pull/561)) Fix `macss -> macos` typo in `OsType` type. + +## \[2.0.0-alpha.1] + +- [`1091d6d`](https://github.com/tauri-apps/plugins-workspace/commit/1091d6d6ac5081f2c7526b0f492ae4f34b306f1d)([#419](https://github.com/tauri-apps/plugins-workspace/pull/419)) The os plugin is recieving a few changes to improve consistency and add new features: + + - Renamed `Kind` enum to `OsType` and `kind()` function to `os_type()`. + - Added `family()`,`exe_extension()`, and `hostname()` functions and their equivalents for JS. + - Removed `tempdir()` function and its equivalent on JS, use `std::env::temp_dir` instead of `temp_dir` from `tauri::path::PathResolver::temp_dir` and `path.tempDir` on JS. + - Modified `platform()` implementation to return `windows` instead of `win32` and `macos` instead of `darwin` to align with Rust's `std::env::consts::OS` + - `EOL` const in JS has been modified into a function `eol()` fix import issues in frameworks like `next.js` +- [`d74fc0a`](https://github.com/tauri-apps/plugins-workspace/commit/d74fc0a097996e90a37be8f57d50b7d1f6ca616f)([#555](https://github.com/tauri-apps/plugins-workspace/pull/555)) Update to alpha.11. + +## \[2.0.0-alpha.0] + +- [`717ae67`](https://github.com/tauri-apps/plugins-workspace/commit/717ae670978feb4492fac1f295998b93f2b9347f)([#371](https://github.com/tauri-apps/plugins-workspace/pull/371)) First v2 alpha release! diff --git a/plugins/os/Cargo.toml b/plugins/os/Cargo.toml new file mode 100644 index 0000000..c4b3e5a --- /dev/null +++ b/plugins/os/Cargo.toml @@ -0,0 +1,38 @@ +[package] +name = "tauri-plugin-os" +version = "2.3.2" +description = "Read information about the operating system." +edition = { workspace = true } +authors = { workspace = true } +license = { workspace = true } +rust-version = { workspace = true } +repository = { workspace = true } +links = "tauri-plugin-os" +exclude = [ + "/banner.png", + "/guest-js", + "/package.json", + "/rollup.config.js", + "/tsconfig.json", +] + +[package.metadata.platforms.support] +windows = { level = "full", notes = "" } +linux = { level = "full", notes = "" } +macos = { level = "full", notes = "" } +android = { level = "full", notes = "" } +ios = { level = "full", notes = "" } + +[build-dependencies] +tauri-plugin = { workspace = true, features = ["build"] } + +[dependencies] +serde = { workspace = true } +serde_json = { workspace = true } +tauri = { workspace = true } +log = { workspace = true } +thiserror = { workspace = true } +os_info = "3" +sys-locale = "0.3" +gethostname = "1.0" +serialize-to-javascript = "0.1.1" diff --git a/plugins/os/LICENSE.spdx b/plugins/os/LICENSE.spdx new file mode 100644 index 0000000..cdd0df5 --- /dev/null +++ b/plugins/os/LICENSE.spdx @@ -0,0 +1,20 @@ +SPDXVersion: SPDX-2.1 +DataLicense: CC0-1.0 +PackageName: tauri +DataFormat: SPDXRef-1 +PackageSupplier: Organization: The Tauri Programme in the Commons Conservancy +PackageHomePage: https://tauri.app +PackageLicenseDeclared: Apache-2.0 +PackageLicenseDeclared: MIT +PackageCopyrightText: 2019-2022, The Tauri Programme in the Commons Conservancy +PackageSummary: Tauri is a rust project that enables developers to make secure +and small desktop applications using a web frontend. + +PackageComment: The package includes the following libraries; see +Relationship information. + +Created: 2019-05-20T09:00:00Z +PackageDownloadLocation: git://github.com/tauri-apps/tauri +PackageDownloadLocation: git+https://github.com/tauri-apps/tauri.git +PackageDownloadLocation: git+ssh://github.com/tauri-apps/tauri.git +Creator: Person: Daniel Thompson-Yvetot \ No newline at end of file diff --git a/plugins/os/LICENSE_APACHE-2.0 b/plugins/os/LICENSE_APACHE-2.0 new file mode 100644 index 0000000..4947287 --- /dev/null +++ b/plugins/os/LICENSE_APACHE-2.0 @@ -0,0 +1,177 @@ + + 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 \ No newline at end of file diff --git a/plugins/os/LICENSE_MIT b/plugins/os/LICENSE_MIT new file mode 100644 index 0000000..4d75472 --- /dev/null +++ b/plugins/os/LICENSE_MIT @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2017 - Present Tauri Apps 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. \ No newline at end of file diff --git a/plugins/os/README.md b/plugins/os/README.md new file mode 100644 index 0000000..d3f58b2 --- /dev/null +++ b/plugins/os/README.md @@ -0,0 +1,90 @@ +![plugin-os](https://github.com/tauri-apps/plugins-workspace/raw/v2/plugins/os/banner.png) + +Read information about the operating system. + +| Platform | Supported | +| -------- | --------- | +| Linux | ✓ | +| Windows | ✓ | +| macOS | ✓ | +| Android | ✓ | +| iOS | ✓ | + +## Install + +_This plugin requires a Rust version of at least **1.77.2**_ + +There are three general methods of installation that we can recommend. + +1. Use crates.io and npm (easiest, and requires you to trust that our publishing pipeline worked) +2. Pull sources directly from Github using git tags / revision hashes (most secure) +3. Git submodule install this repo in your tauri project and then use file protocol to ingest the source (most secure, but inconvenient to use) + +Install the Core plugin by adding the following to your `Cargo.toml` file: + +`src-tauri/Cargo.toml` + +```toml +[dependencies] +tauri-plugin-os = "2.0.0" +# alternatively with Git: +tauri-plugin-os = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v2" } +``` + +You can install the JavaScript Guest bindings using your preferred JavaScript package manager: + +```sh +pnpm add @tauri-apps/plugin-os +# or +npm add @tauri-apps/plugin-os +# or +yarn add @tauri-apps/plugin-os +``` + +## Usage + +First you need to register the core plugin with Tauri: + +`src-tauri/src/lib.rs` + +```rust +fn main() { + tauri::Builder::default() + .plugin(tauri_plugin_os::init()) + .run(tauri::generate_context!()) + .expect("error while running tauri application"); +} +``` + +Afterwards all the plugin's APIs are available through the JavaScript guest bindings: + +```javascript +import { version } from '@tauri-apps/plugin-os' +const osVersion = await version() +``` + +## Contributing + +PRs accepted. Please make sure to read the Contributing Guide before making a pull request. + +## Partners + + + + + + + +
+ + CrabNebula + +
+ +For the complete list of sponsors please visit our [website](https://tauri.app#sponsors) and [Open Collective](https://opencollective.com/tauri). + +## License + +Code: (c) 2015 - Present - The Tauri Programme within The Commons Conservancy. + +MIT or MIT/Apache 2.0 where applicable. diff --git a/plugins/os/SECURITY.md b/plugins/os/SECURITY.md new file mode 100644 index 0000000..4f09bba --- /dev/null +++ b/plugins/os/SECURITY.md @@ -0,0 +1,23 @@ +# Security Policy + +**Do not report security vulnerabilities through public GitHub issues.** + +**Please use the [Private Vulnerability Disclosure](https://docs.github.com/en/code-security/security-advisories/guidance-on-reporting-and-writing-information-about-vulnerabilities/privately-reporting-a-security-vulnerability#privately-reporting-a-security-vulnerability) feature of GitHub.** + +Include as much of the following information: + +- Type of issue (e.g. improper input parsing, privilege escalation, etc.) +- The location of the affected source code (tag/branch/commit or direct URL) +- Any special configuration required to reproduce the issue +- The distribution affected or used to help us with reproduction of the issue +- Step-by-step instructions to reproduce the issue +- Ideally a reproduction repository +- Impact of the issue, including how an attacker might exploit the issue + +We prefer to receive reports in English. + +## Contact + +Please disclose a vulnerability or security relevant issue here: [https://github.com/tauri-apps/plugins-workspace/security/advisories/new](https://github.com/tauri-apps/plugins-workspace/security/advisories/new). + +Alternatively, you can also contact us by email via [security@tauri.app](mailto:security@tauri.app). diff --git a/plugins/os/api-iife.js b/plugins/os/api-iife.js new file mode 100644 index 0000000..48b41cc --- /dev/null +++ b/plugins/os/api-iife.js @@ -0,0 +1 @@ +if("__TAURI__"in window){var __TAURI_PLUGIN_OS__=function(_,n){"use strict";return _.arch=function(){return window.__TAURI_OS_PLUGIN_INTERNALS__.arch},_.eol=function(){return window.__TAURI_OS_PLUGIN_INTERNALS__.eol},_.exeExtension=function(){return window.__TAURI_OS_PLUGIN_INTERNALS__.exe_extension},_.family=function(){return window.__TAURI_OS_PLUGIN_INTERNALS__.family},_.hostname=async function(){return await n.invoke("plugin:os|hostname")},_.locale=async function(){return await n.invoke("plugin:os|locale")},_.platform=function(){return window.__TAURI_OS_PLUGIN_INTERNALS__.platform},_.type=function(){return window.__TAURI_OS_PLUGIN_INTERNALS__.os_type},_.version=function(){return window.__TAURI_OS_PLUGIN_INTERNALS__.version},_}({},window.__TAURI__.core);Object.defineProperty(window.__TAURI__,"os",{value:__TAURI_PLUGIN_OS__})} diff --git a/plugins/os/banner.png b/plugins/os/banner.png new file mode 100644 index 0000000000000000000000000000000000000000..08b4fd8c16954cfe1dc6434c21d82aa93510d271 GIT binary patch literal 57150 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

>v}iSIjhYm_hWy=WR;+2Yr))cJ^{7ygyW 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�t%)d*s{76>RmmMWaK>=(ML|1k`I%slMUbdr zXagEN>V=Kl_1seC2f?6MLBSrY9Mi|4(!#Ka{Lts=*yjgp;N^KEvtJE^2}u)mU81ug zqC|-3=ZLh3aV4KFB2*4QoD_&4kEES#ozX($4|R-8gs3(5V|`Po73e(Ty6Z+Ui|x^+ zWcKI}>ig~1yONx4!KG8c5HfuupAHi62KwT&8**grmL{sho0ac4jyTi^ zOpHCHLJC1`iD-DpRicNS%l;nthU-Fz}@ylZXY2$JS0Z=xI~Lmi`1dPen_lZ zHt9e%4`%pmf#E!5}UQqt=gg)S=J8CgXx;i@XrFRg@Wo*Bx`!pw zY{2#Gka)-@?PI)1!r3h_gXPmdzsL#U)YH;Hok92`-Shx<`6t}z@G_$_$(=6~_;4+0 zD96INf<7~hF|+;XT$Z1}T-E>i65j!T?Hc$GPuTomZf_JME6{NB-7tM(#J^`IJ@Kyl zbU?EFA^5W&Li$=YlX%cF1zA`tZOa1So-WCt#xcb#a#e9OfB1ucgoy$~`BwkYi8S5N z>r^o~@mnLI)p78VJS@KOy%9k~iR*^*OT4&x0jvJ zhAB$)hFB$n3_@#aOq!X-HLN_p26nTcyc3Ne39Qf|h~!G9Q=&s87VlgBZg2V`f!&aL zS|##stx3Q~bposY9jp|G$AOW?nDCJ2iuX3c()??L$6*!=aKjZBnpG&B?x3F+pW+Fim|Z0A&8cAyC_j~UPkYukQ)V0wFH*j7uEJhRAoD%w+ajv}7}=z1qJel|@2mEyVS8 zi$x~ZpSc>!T>{RHKigi!qbS)*C0TJN;~H!3>bWzF0=ziP15Tw=)s=4Pe|>r!+)Tk#-m| z({Isd+4g;yLP3l%;FD;#PLADlz$WQ*+vR}*25wSI#OI;vSQE!rk!F#x!ua-o#dRzU zO}~oYNL#LZCO^UPkFZ}#UaL0(Ecx%ZoBn>U78d8_DRjxoM%~SSYKNb*?(y%AB(L$D z&XPOan{jyRW;&^@T(UzMh(qEcYKLpbOxKB(F%0v`4nOQAjVqV+ZTLjbLMct|-hl#@sz!>t5XfvXs#+A`i45L&eh zcY_JF0zqKV7)j~|@-zvOF$clBWP?c6x*XrZIq2k!4LBZgw?m3%&JP@q;uW9ROoBt4 zxR&I)DztO#Hz9qDdy?!H55}_hfKQUWZs0zzhmnElJlF*9RV1hXgO{yXI&D6egGPqC z`?z#641>IMHF&bW(9tB(H0O=FkdXbeFB})$H`lirAEkxrkE9f8W!qZ&=OC#zKxUHT zW)2<^q6km8XAgS^#xC5bJq~FFpSF(FfE-bd9VZay9S-XsYx!=A3AL35t|3nB z9}Y>22e1ts<$C^5uJ1g1~WC2t}@kW03zUBG>bTUF2|O#;t=r+D z&3=V{JWT3daW4Phi{ytXHxz^xbYBN2OV#kL(jK#nV4*G6Zm{i%;D*)iF|P$^Ou+46 zDBKpyGOAdk2J z31gRMPj3SpMvk!%GVi8rUoiwpwTNvbx$^C$gLZ_SxGZh7mihg^6VnECqgWeU8yI`| zt=0*j#5B1%?8R`uhbe5N=sOF&j;52irrdXv_JH8nrc4hlqEIxV>B#bBhal`77>ST& zP-aw#B6YHnq6incjT_}$I!s?{fn8Ap^|WE1U@r9%#RBPT$Ti-m#F)XcL1Azt$_Yla zs`dPIfQ99$!SrJT>hlomlJ$!AW(zF*5j3de4}9<(*%M`oaYjYw+&YVZ)Nm%J(xPK3 z?3JQ4R@`jq&khT?hg7apR6<(5dYsP-4b9xQQju78l~uI6unw}HM(_w)dOBDf8PU%w z^3Xmy73?6bE{d+)w=+z8z}_Y3QKTtT5T;69fYcRT zh#OJaYWV7Bm3UkGrN5_qE{U-?yDG*ZYCHO2q%}i)8PN&x71kN$xDnY|g>*S*0MwguAepcy2f9qM6+Dn31e-ED=Qxb?h z5E*{0RFL5|vYe|f|GkT|?`!>047_>lt3cY1@=zKnw6DPp@lGoHCBX&;AvK<@vf_C0SB{Q&OxG1wHKk4^q=g|Qrv?)%-y*c~9Rm3S#^v}|z z(qmTc^oo7-ISM7RNNwDrD8MRV!i^$hE8Dmw15YrBP~tw}(H1pE8D{6eRYP?6+^FC7aRLLE_P{)t1^GqumKeC+~{ zU6k3(+1w(vG`vdW!%RuBRauwtaV4hw8Fq<#2`?$Q#Bv$&Luq~3Z|UL_50`>GF-|+K zkjj?NqZ3Vbk*|7Q!vm)9i&Om{P%x*=IiN5{{CSV|>HAypV$UwyE5V-?k>1GaS5I+& z#PMcl(YzaT9g-Dj zX`~ep=~M;`HqapoDxok1lokY(ZlnZ}l13?|1f=n~`+Q%&XMgY)uif2y&pqef6Ys;Q z*zAfnIPI4yn>|A+{sTF`nFY^%bx-IRx*SIFU0QE%a4hoculsa4Qq2)h=-GotrH~8c z^N^Uylh7BFddLXM6U5&KF1xaij0x%~&C-%tMTj=qO3fYXtm5}7$WPY1IuY(tx_>6he9({Z)T&gKzN3s(rR1>Ro@dTgmX74J6Ht_K5`;#wjeE^@PW}eodtMdgI3&61d)I z#Lq4F0H%|$;NwLfh-qMpv%2QB-{i|!E%uarkE`SM5*y4{@(>m*TG>k6D|Fc!5X1=} zY@;hfi0{h}Zs&-L0V1Zhe#iCQs-D=TGHxooA`VhN=f_!SnAg&aKDvNUuRe|Pl&j`@ z#^1B4r6KZ{#Ny9mi`*at=~GYdY?ZX@)70=pXh1l5V*p$^TyXEiy?j%%4mnF|kxJbI*5| zi5P9TK*!v4Obu-+{l`*T`H`F8$-ffQQbu*?n1y# zLQDa3m8psFWy60@KrJ4c(iHpMgw(Q`%|w*AUK4zt{EL5`xghG(m1J+8uJ8iuN?JEl z=k{;L8Qt@P!G9DsF0VxiZTx${+Q8fV-nm354t~M8vYz!644J$1Rtyu=|rcIXlk@`i`0cz_2U6E)m+g8TL(LJ#N*xi5be(*jI! zF6+Lv483M~m+^|Z3A*fRv3AcgS7k=?mM8j|i3ziJ>o4X%?){+kLP3QNuKGY`QF50m z$hcn#)0?gF&nZXlK@(;P7R0&Bu|HrSk`{(@Hr6PPiTO)fR{mnACm6Wvng=x)e{u>) zDEgOOEacAR*cgi-Mk&@3D=5+1eGwUQd2QuA0qxv*jn~-Z&F}`!iddcAiT1^-G}guw z5$mW)=LR9ynmR~JkD;1AxvItZ$d=@rF{98EfGbkv0-T!QX0oMoV%6C<_-DzApr~VF ziI?K-3_XZ8Ro6Tg6}OZ5Z!0n2xwCO);3XZ!Hg6feDz<8_nqa6xvBUAyc#}MJ+Xiwa zAct6OKV!lKZ9?`ju#Hkf7>Z`>N;LYS`Lxq84&=I15E7}5aUaFdLHuaO^e5sg^K2#d z+|%(MoSSjqD98F(tKeg6F~cOz8CRM8yp!)s0G-p(XX~mS(|?noM72utt5G2g{WyS0 zv%*hr=jQq)x;-|UGo*ss>KyRG293&^56_5x4kWzffm=MbQ%0*E)4NI@Bt~k*cG`d9 z9;JBW7=VDpu$^?O+Z$8P{LbZdM&_(bqqofCV$hEm%~NMI(Lb*gW{-S1il6#`Opz~` zFnh7DacqS#+s~$goIqyZ{>$5^2~v;d#y^TN(0F^%lp=}qE^21iIY!sry{t4*DR=c? z@nCnRUqo?*S9IhKPNB;HIz^g9v+RB>A9Q0cFxw2KuP7#U0GV+zQr#3#oV*qbbIG7O zB?4Z#h-klG3Uch3ggU{g>KmZFnos-D8*WZUm#60g*=AS$a}$UT(%qmOj5pHPB5~KB zv)5-|=9uh2b}Dc0i|V`ZkVmvX00ijf#@68-L!RHDj#$Y-_8`%7(o^GPvLR$aCHd(s zbvN4f=?U!0#a;2U;@L_iRjI?%~FTb?x)P;a-pFm0%=@_O| zHBS}fJyT40DtSeSCPZnoZfOMIk@2+S1hwT;`-FQ>8rHSc9MIj4Y%#?%lSMH?T@mOR z;G`^B2vjtKw_wCO4~MWPUD1Nqg@(&Y!!9A@5vVUhsEc;!ru*~pmC+!K(q99Ap({d% zz&-N^II^QAOX?%yEt#cCrE~=~r`Mt-sJ!A$wU){gdMjMX?H&_Dkh6CB!=eJg4oWBHC$8`gc8ZjBX-F zm#@w#H@sN2oDvt&h0AtVm{6eOWmmMi;8^4fwjmi6p+-+~LQ~ zrDaCgI;|h%YPj7z;UooR;;oN0j}lpmw)JFG^s zEx%SVdP%@HAmrY$7HAe18fjBDBZ8Z*YZ}~C|QY% ztFz5ntfdNa4d$VTk&TwpCcW-)ZBg1$8QEJJMH(LYWW9q-*L4v?CGWxw7%%lHYEK}o z_|xCtxcbxaL4X05zXb{698Ma#8`ZLa7XB(st@+WH{Cknu9Njddg?bP!EAQD*?_bUD z>E`Iwn^g(hzsvXgbyj~)nsJbsf68F;qB!7NFSZQ1K=M3j{^W?QP&AhV$+}x2-u_DT z<=L~oU&){MK6&g{lknw9U&z+js%E>L%!^wP9MK}?nC&r)C6we;Qdc;3JH9lra6fGI zV6KUoWL3GNYo`NljHZOBG>n0zx{-wDOH}~Ac|%(PYak-F3yA@uqfl7)i%T(>YS_s4 zWKDz^KVGw#VMF_edGL%`{t|>0wS~R^iDoA=)d&-gE0+GM;hH5Nom3=KmO2&xZTRs% zub3dkTEy_T?4ZWyeFI*)7053{z}Xw$Dfm-oetoO?t>hw~8(Z=(9iS0VdelzJP98=A z%~bZv_6Nh7-4Vwz91hhsmJ@YWVmYILrpHXb`>2D_fHuw=o~;$>ex8%XZW}+@(IJ;S z1L2pD-UkjMa!&7D`lS;wX-l8HGvV19ZmB`qExI>e8LNJ zLY}8pA%8D@Qi>(V>diYdM8BWnsL470+qv?iLgzd0g}`PGsH-g^>jl*8BqyX!_^bVhDLiQ znRd*@9kB^9IYtTHHq=WZ)(RhGJMBIn|50ClEhpbZt6SJpz9-w{+iC+~IY7 z!MaOh%O6nTlJ?VcD&^n1C(;d;0dahWAHUZ`xILD%HZmN)B6rZlEF! zZX41(EFD$8O*wLbahMdNUz0y=Q|ElsQ`EUmsIBg6b+=1L*M&h+;gOC-0&; zC}P#k_^%#36c$OF^IPcEV;shMiD_ysbG}Wsk{9(!hU@GVv0j`IgYB+K4D)cbp7GvQ zTCc9XtDz)c!or6>8n6rY0LT+Hx%M;~wXkN|0EOU^FBM-A=AVa7HBMo|p zSAg^UOn-ydNu|GsYSm4=YRj})ejj<23Q4R3agaG1~S zj(%tD`E>59e!wh@ygvwO!^#EA;(|4b3lv)BtJMO_!Mb8Lut7?!t}s zz_`DKu_5fQ`+|2H!R?9aRcU~*j--U;sTlRLRDO1NNO~1}(SBdHFwpuVzBcbF^V~JM zW1iMLCrt;w(8F6pkFTEa8tJE04|<_9dTj1P<@56sYs&I)LNLQ*)l2s=$Kkj1#sWo0 zx)vu|T+uOUS|5)SiWn0^$&>Zu0$)V)xCXsNzJ=22Sy@Zq_F;3y4`FgC&>=)yJ!lV; z3Vm_!>AugUKan&&(?;Z-A?v*i8%AijHe`eKpoH1W9^R`+QEYD@HJu`ZHtE?)N8DlJ|5@9gx=)Um0n6!HzhW_;%lsXa!3uIU6)m25b8ZiR?(Pr%eA!Dr>mkg*@fA64cmthNCX34 ziVIa~CVM>#4$*inv%p}2c(!Oft+xvgP$l1nrTkbM~7xUKzAb26yY3Cq_SKk|A9^@wqLVQhPOt%bU>no z)3S-N3h7;;fX316jxTC*a@UV{pdMbV`GLuU%T3q(VSIF+__~mF1;(EVQqQc?H{J{g z&WJCnde;LuaeFo3;uR8O5o!iJ@d0r*{VIzcg0~a@FBd?5(Yv6!l1diYFf0Vp;?KM# zI-KSwFgPj%C1-$US`>16Ssf9@>k79+v`L2s@l450f7K`sV%vALPyMj+DLHH@GTF~} z+4z1>Qe1Z^@0W9s?O|7HD&Tfms*uX<>94L=aa20Ta&&y4MrsdBMBZUtF#6TZ`+00j z(;Qv5s4xwRo+GVyd9b|mfWOz=>&8lQ1TronSu*NYvPD+3#BRWUt*!X^VdAp+1LQTg zeKH(hC_RfDH70j;)TH_u)^AJ1&3z^Iw3vPf{S14&7rlA%HTQ3|Y92(_-r;Mlaab8- zV3w-OSmnz7wLHaT)%{|#fgze>`tcq`kAhQ>)33-;7r_6r`P*A+&PTfUmEr=CdlGpM z+1t+&gS1Xx>qQcNlu_V0%<}?!gGX2v`rCIOc~Ts}?!%zjAuUqql@OVLhE_bU~~PM5OzsW80-D&4#m@8qW5oj?YcGS{Y;Q6z~b$0{{_ zeP~~g-?GwG0qT4Bsy;9BYO#Z~aF%Z6DJ^0y7Ta%?Kwh_4=;UMtneb@}jsmyPvz{2! za^7s+Dh^))Ro;8dxa)l9dOjdX(56jN3DME$a)u{hIFCWXLERMGwY>4~=fpWZEH(Tf zP5pS)FH6t3%f`%Z`o;*vjbE^oAD;=ElB#3vChzj|k%3zfQcwwDofH)DGUj?P`{GU` zCQFHe7%Z=s`8)cBk2y?Dr7cH9p)>_W(lIOo#LTMYRco3l(LI|(p!50*OQEX}N@bFE z_1J$zrw8^0ECV{+N3oMFuGsv^2q~w#>Z3oSi6zeo0|y{T zWt-K7=Os7Ef3Oyc;YNxfglnkX9>k<gWhpn#W;5D z6i{(ohU+LPbXcRM;5_dTtp;a?R{YR@s#@H4kh&p@l~x@;JOEXtPxFG1Mt5=R{Vb{n z0qR;f_a@D1t%^^8?6+&PtO_}U21DYZ15m8crOodE-#PjPpHQi(9GHFO`F)gGq)a9P z&gagDyE~UrUW@^s^7)Gxh&E!;rFoUI8irHSd@D4@9RZB^{;y zx;2Qb;MdVcEGS$@tBy>{oUayOdgB>l!{12#O*4+N%5^3~GJkP0f`wIOKb+%Pl?jC7X>Z54V z0B2$fG!%csxwMN~<#HR8&CV{kj0xWwKOnPy?|tq2v2xUj@VTLrmNBn#YsSzBwM~z{ zY}iVAMASF;$y-JYTmrcUW^?d3?7yFD}4uAmYk^VJ`d}x zyxBmXGdjYzxCW#-j3q|u(5+7Y9<`!;J_qT=#8W$0J;K?Ko9nl?y(p1>`ADc@9X(ZB z;3j8$voW#zL<2N-{ zK4Oq>X<$EVt>Fj4;P+)s3uh?cURcOSjtSsG;DAz;sdxxaX%h>UfCPeIMEfw znzshKox z5oY_MRS(_6g??Ma*fXK|@+~V@uaYZ&C*-qI6;;ZfZYh;Ul+axF!QHg%NLT@aH2pXs zoBp5mkYXw$Wp|5Yzs(!krT0)gTG7()d&vAv)DY61cPzj(TO~1eDzoREE|%q6jt+)K zFols!cHDgE!oUHq=#qJlC)8o_CiK9|Z7^@kM~^`NhNnXEXJozEO-lIXmCxa|ck>`I zUOH*rjl#LC9ntv%$>hH7&MS)5h-hjO0t@V$k_WOyTKrIbQ*3S^9FaJ_P9ODjbn=wv z;LUKupwccEU)E?44W_a+{Hw;1pGbg4z5kw87gCWT;33DiqGi=O>phaP_vVq(`T<;f zQe`h9JLiHnrh}$L(GMd6qG9}sbkXtk7^zORMa62R=(2BK=U&!LjD?TxXmP%t)3ZC# zThWYtWm3240nL}1lcESn6rPP;Zn$CD8>CJZR@(obWTl54?Ys1;&VSew3S(R0EWeTW zV@uJ0lYTM!6O^o9qMuq-$nI6v-+Dy}g%?AsI(q`ksfrY_-KqoG$Yvjb_zSz-8yzaK zmP|`2bI>%@zOXv=>QHrHwZw+mi!VcpLwZ4?8(pE&9ntV40jRwlKdu)G?9M z$-{9cW*W{7e}dlb=^WG3k(F|*I@X4PxazU^ZT%OrC!04`E^SJU zPpyaju!EiOT@9K;JkLG(W;pp^F?1vpyKA}^JM}=mKqCd*3rFMS_(7vD~ z#{1u<#D{~!uYHlKJ?Cz7#ANp_Wq6GXgU5lm>TgT?d{B=b(o6Mg2$B7 zUzLt{$;ErhQgBccnsP4*-#o$!iP-|_d_Q|%M4i3ygRIeXnS-Jj2}SVpiJxb(5G-n6 z6?EoY!#JUx`~=|AcGsrF^J6N5X8|)-IS_L%8SFxWYon9JsB$>=9>r`4PN)u4PXrSn z|N9DjGAYLr$i!!fA*n7dy5FbWlbZU!FTogJrfSBovn?C|pJpM5S5YypeJ2@+6HW4< zVZK7y>9nR~zNK)TTm-s?vwiRJQx`hiL@-nM{{=#VBz{-`A;=lTBs#eFrVAW7AoUo# zWLm|kKp7MA4;G~a?6{eW)D7I}^;70G8suEjt$0rnET7uK|8^2=W%6oE{x-K<#mc(lQ1(ki|C=7!*n{_; zLxaTXk%iEm#m+YoENp@K)m)^KCv_lidSCrS4f@|=vk{6?M`?}c-`s(C^Wf$dF0m3* zH+xu__`w7KqP;z;h>=xw7>2@N;P z>aOyhu!=D|-=90QpiI~^4Q#jVVp_IYUt1z ziY4#%K0M%E^QSC3<&Ig`dx1y}g^y!)D;(x6x`A0JMjFPQJLn3qNvJL1%gxZ8ciSS; z+}Eb)?1dt)qMeMsC-2b#@w^24#=RX!`6HCZ6DjH=%;-%|^A&mK+R=3w1;R-*#~ENz zjGa?iCIuF+=M2^}=U3}X`N*F;Fx`FkYvaS8p#7t<6NiEWSM*(^6f5G{yN9fKB6_se zh3R@1x=>nETAj||yyE|8?xnIfcVe8z?coLVOA(+FI%n{X*CQ;-pSY%wMG54TDin;p z6V@(ZS%}4;lF$%#SD{}rxnjDN3f~~s*nBZ0xnD4opWm20G~m;#?`}8rX$!SqAgg6= zGmr-Eue`N1CbjbDb#$3vDhdMs|^PDMpI@y{2~t z&}hWji;ilacJd|$UIS&EQs_W#ge*r6D2}54Z{A3%O6gpIlHM>~yB|I&YiUaft*HZH zRE-6jaaKmzX;V#9wYu6&WoaqmZ<@s4WG}y$@f_@rHL_2D^+zH#e+`R+(JfvSk=Tiy z!s(KG!dhbPfpX?>`cB6+K(XZ6yONAkOKBcq%=%(m;d0Y$_)V)3K~0ihe^des@4|zP zQ-ixc=^6DIu6hoML?i@Cy;VTZ8l+WVyc`|1s&6AN?WV{}=w0jf#L%RL&`M=}5Z5#M zuU1}}@00-wE6xSDI@eSuywrnAuHc4(lAK65eik^Z)%cL_FhyDJWcjF1NG?5GH;&8K z84M>jflTVbE;&#=t|6bpET^Jo4=4X{mO^h6OEQ1Pvi_p%7Rj9%J~!9M3YK!*)V;4} zf&BiLXbp;)5}d}2JFSM`(%jBVHzPM{c4MC8O`0in=^n=eO^$}dwzmkwOWR#B7`|DA zD~j?m6nbew@%?>0KOAP%ZY>+i0%g;Kq`*O_abqMIF*t-P`;OGK&RR@XVd`0Kf(jf(>?e)KXX1mk`Vmg z`TVx+r@7s!B%B}Vt9z(-&xb0-+<%NVWb5T^9U9Xoo}tq`W1Z6qxxY@cuXYWh=u=_F z9n=T3ZbA~{gT(0#RHuyX<S*338W1K*j6jC3iw=l53AyLB-h76{vf{Lzm$EVK?fM zxg8g01DeC|>$sDY3Z7!R@PrDU1NHnm1p5SrcvjhN+5F=ruN=GdpQcsvO~K$;Clr!5 zJ@eYnnODYoGWg+VT07Q;3CUdLxmK7{X8c$yq`!?yCveo035wql93gw3m{M({N}aN; zic#K%^@N4)sy?sKOS9__Yi8W(Q(HOLKgw|R@yc#H6om)uk#xoohZV5-+MlCh4NSQ< zCSCWpeSp`kl21v{HHnmFi>t6wxDUxvhMocbFDCUG@}y3Z0m|l-wFNz&U`pDyE?Mf6 z;jIGZ{GH{Ou=^0sE(m$-F-bX!)+BcR;FRNj*1lZc&s*M9#Bna#e6`X)wOAsKr0PNs zj%@bn;CTXrt`9l<45aZJUUXw-jD(3Wmm%#a3t~t6Jp$c7dxD9fYb*$VX@_g6ehKBP zPV(!2bkc3wNsQz^P_|IhPo2k*7tU3fz8spDY_WTx-eyF!CNLLT8Bi$PkgIj5yY2A7 z?-{rtz~Z<2aud`-KCzc=!N=?d_*P z&0J!{OC>gnn>Txr|8|>rp}4F|k+fKzL6JBh>1Eg9Kv1)Rbsn}-k9FcPJznv`-P38p ztk4%)gu`1+51E?FUJ%&!@E97QGnuoL16NteVYvmDaw=e1;b)0*mwq|%Odo-#6V4EnT4MBj1PpYuHE3-al?qUf( zAl*&I@3YDp`7up2=3K`KQ6;#p|8+b}pFAz5OJ(@yUpXKfYXatrXg>wMQ}{XLKXWw@OW^mG33j)Z>#febi$9hxS)5Q?zEE{tXzWPhtV|<4o9404agC7 zq6#_nn?bBSh1}Qqp?l%YRst1Hxc~Tpdbj6lET#p_3EmhNEZOEyh`P0*HF+7_;ddw; z;&z74pPv`w3<1dszoRrX>kLY!S_v<}@BKFJ6o9F00x%-Aw5r z0P#ds$h{J>CbuVb%w?K|oN|u4gbtj-=>B-OPZAP5GVX=9H+ZYohZWUI`SlUywS z1tuqyt{>I{1Ca%Ek>8q;JZ83}R6eX;%P~dkDR;^bQQwW=FYz^Kz1}TR7}G zrgXE$c$X!(+W?0b!Z=vXCwpjhC~O76uoU-(tPmKRYSOev$dk$@XNT6~P*661Sp&`p zfF2-H+DrbqsqGBivkgj@9TOrBe>zE&^2v@TsmF>oJ(qs?~rJ>na5Xs^NM`k z*QnWUwDB6fU4h(3Fu>wz;gZo4vbVvyjo<(>OQbMjS?xOKWWjVY2wy<=Io5I~jj{%= zxz~Xdq_X55C+BqQ1_p0h7Q;$g*WbLn;A$ISUS1{vm!J^0uwQFHfY1zcAS*%)-@JDl zK2g9OfFyNms5Ao>6D3!|1BtPf>4<vV$bd>;!}D8fB|!`EIGhR8R+0q zzRvDHI}bQmeceQnsol~0*)`~e3PAM z74N!4$DO$Xe^Gc&%g4BV5&;3GD5M~ELS#wyvfeqC1{jPHW+>mIk^_+u761YW>-~5Q zSPRvK(L@*Qz6qwO#2@vTW4H;vMA~KV2)hlrU$Esi(YfOSiusS`Tt>~wK^EdOQ5*Vs z{x0g_q+lsu*D(sXboiq`IY$>T=iN~7lNrI?@iH#oe2bOD2o#FTwN6UrB@^`tZ1+#S>*2sC6$`9X&tT^(*h zYQw{n%=!qL&D}4J2r5hWt-h4(OPw#mwA@wQNF|88JT@mAp55P`YH?)l?yWAgFLek# z?hI?Wi2Y9|?)U}09qW$|I-TDly8v{gnnKU2%M{ZL9J5HmKS_H1+fE%~=L63ZP5h{C zddT4o&^_EV4Gl8t0e0A9N2-=n9`ly6Gq5XNSmCX%T@AmOlGI*0*Gp9R-h`vL@cT;e z5w%aO5Vkwu9E#+7TxpZ&;)8nAEhQEmZRZAqXXr?5KnLG*5-E|YtPAE3sq8UL?6vOc zul_NfrM)785+^6(LC!g^)CH*HDb0W>f9J3PpmVkMGo(aTRG_^Ya4M^TxlchJLJ*NZlVgunIg)@lynP`7yA~rg>f$CM1bm##KuOJPUC~SH`;9 zzqqa2;g_BH#on0RMF;?PuuZw-!D1y2L`KI^)>xOmsn_t*R>kDehq%H33ISS9XsbCr zkq(f^GuA_;qNr!FqHukz8>2EMf+X*Tpyz}gx~H9&&G5tE5~3_%pUjhEI=iAyt*8i5=-_#qD^A9kNpF9DD5x=H^HoN5?#!CZnH-kU71S&q~{*i z8jkf;pu0J75Ozo3if2+iDh+EaU->XiPW@NpMi#sv0jL0D*fr)@T|2}GLhrLD!VQUx zpwImbtAP*5{9>_~;Jda~BRawcLrc+gMprg`nP)C)C>*#CFsFOfnS!ON+1=W_SkkWkJ6M zoez{Lu^HrUmvtapqojiEmrlsLxHV*E?^OY(5*v4g(RP2Dh492)Phz{w_x(x?;j`oO zXjysV<%`QyWHw;Cz^SJa{O8)J%8G%f@=axWh10x(iWPK&w8JB0N;J+6pMQ1P;TJw7 zgMJAs8-S}!pk_7QV#F}buYkMD%0ED{z_&tue_W(yF;;oa*aJ<&PYEIUTa?f>Q6;-M z2&CR)If`gf>E{*BH1t{pF}ly&!;)!Pppts67)%Dep4iBssfmfnI3KOqPvG~szFtKhkeGHU21+qBmo69dQWheBK(QWY|5?}l zp41;}tN4O1jFhEY9T28u1jvG&a>lNx}F?pCr6Jf&^0x4%NN0>$nz@y zEREQhb2SQ&aZTd)$KRz~CioOzULX3a`63c5`7xpsT-c}~rzU3AYX0Lt8M zCyVO@h#}N3h}h7?C3<^ZD&P)qHhHj$HOI;szEzwOwg1r1bR8Z|p#!C3b*F^;#8t84 zjAEA6e8`D*%fIy!HD3}9C-R?b5=M4N9ji;AiuB>s&61eVAf|I{zSh;_>~!+xz{z85 ziW$rc4w_Iw@EN|uYmVZ%fDJ{W!=fh!gxx?Z0=PYXy~ENZx+?b;HFE{W+0wg&h0ue= z8c_-R`-evF0-ZhiFSoC%Sd>`61pi|e-;iaj>lIcgn+Z@~w;?e)r*iAo1SP-fqH;3P zmwOe=&?`wOqlA}C;AHet8THq~r4`!%yYf4cEW=%hr;7kU&az~11Pv2C#YpMe{3kX8 zdR(LXgKQV9k#G6FP!$(`ZU@)OSSq8C2p0fAoQ z9&4ZA3cw*x#mXKgAAY0bgV#_cL8~rafHo^yVS85N*#lW8H7TOu((E0s=?CsV%27rg zq$#T`UAWv2;3juJfT`Hs@>UFO`_rJ3o^7Zc1=~9$Gumw$fFs^Z`thfO-C1!iaC1-v zZ>R>-G}$5GIgE5f{DNJ9?>i4tQJPedOZwM|v$9cd+96(7hkfznD&>fUo#jZ^3d{**f!Jx!jw#cr;PUw-y2m`pPgTtYO%Y?~@zPyP{ z#@sWdYId2pybucDGeFNGG)I3fM+5j=IUb(Qv!iehK;+2h@y_u9I-hSKozaeCor?Br zVdC%s=W}O&&}F8ctQ$JM2zP5|fXDj7_H27r8r$eb=8~ZXO9Ax$OQo z8=#Y7x&z4<#*Gu;OPz18zbkVJ>cX*&(;Xw-Wu5OE7(TO-nt%i@YC#^s2NGZ7${VwcU!~MxO`s)Go_Mk*MH4V5 za@gjnAK*>ZQ`U|>LbEq3Tc_aUV(shi{>+!NFbplTt66W;%1fnDP=#&NkaE+!iDvPp zSrQl_yf0#a;+G{u;d{V)K1Pna8NrV=rh`?p9UxEN&uLz8tP5D!a|C0$-%Q3u60=Yo z8Fyudv<-QuLGP%^4=>&7G1b&*8U0r}m76F2>bk4i*7$t8l{E^9-Li8_l@sULyVSeQ zNWrT5Ct4FTDkSe`{;)d&FysbYW{KjQb_exjpMn&k{Rm^sf{TQo7x+=!k|jcM)~YkX ziCd!hOP2qKs2aWXd&0QI9UCU(DlzxSgZcm=^-sieHTgk)Wos->&Dwn?^xNC(q2)L0 z8FmhGwes*Z3eZl>_Paw73m%pmx}KLDiIKf!6N~Qy*-A41IF?xS#8=Gly%!6Z%r7k< zyB!HO)HlIAIa`X8JvmGtsjdWI_&;W@s*bgf{*;}vMk?PwN~jm8qI$GG`5O&)8&U(g zURys`n|z6@n+qrxd9eu+704FrlePYRYCZUSxTJ`dFuZD4m9 zNmIeQXXHx1(6`{2K*3?XE7E_b(X-giPkKLDu~%QUz~x#e7_N)~^cH)30UEpjQNEBJ zY%5?%-RgV#GqpIV5JO10VEltI60&^B!bRF2?e-QtLWSFc{D3YUU*P|f>B;#w9x#){ zu+2ctj?gM*%sZPr6_fU<9Fl+kwCC_WP;Gr85&oYSw?HSWJD{dbc9rq*f&f|+>BP;% z?yd{?wtpIA949K2fW-LdJoiL9<#WS7-?y?Fd;w>WFl)9jhopv`mm31Ce{ zui5#Q)+oM~1qU}{!42T*2DdP_V<*hT^m;uo2?F9?0|rxK3TwpN!+i9BHK23^;1*b* z%P-3Q66< zCeRaeK(g){r1Z?~#d3cmL^s`iStzJuM7WOaaRPB&ztwc&k~^So>(7`j8{uvuPvG;! zfOS3G=ZWqmcA8bibYwU?P_-`nJ}2Tf70w^0FMS{#Yr(RJNdw_BjAyv=y9V*3Avi`? zFva&qsa^2(Q@4-TMw-v}%SDJlZ;d&SdiT9d_%gNvyj7Z0Y~lM_J#@%8a5e)JsSP-G;d zsIXO$Na(DWfVFIEr=c2AK`T?*NV|l6=Cxn>cWen?_zU8IY*j$g42h1-9eivOBjYU^ z%<=U@W_%TR=|+H` zlVM|(jOF7Wn7sHdTiw>8g52DbC%psF)?Os#+k6it-PNXVKq)pS zM3=&EaBk-em;2nAFYL(qFaAZBO6IMj)Q@FH)Zbn;%MFz7{3xfk^7a>u74Vk+7`;2A zQy(;B=x%V3qEnihn!50cB`^W@SmhnR!Sjo``uxs=psR#&+oQu@ds{O0#&O>p=1^=&7UbVX1)<37slh^)mty6}cfQ7BF+V8v7yG#6j1*q(N| ze9^$EOLZkkHrLX++>t8{r0M4pym|iy3`s=Z8S5||Pk%1-`d&@huR=>hh3nDNH&`6T z_l>T+9;e%%@xD_TcWYQ#{jcqad+qFjh{2Q?+)d`Q^%w(xw844Ap}-oinEs3MO zROxh=chKYkwcoimtrf}LMl?4+rt^EV+pI`?1V=}oz0uZZkigxnIWZlPt!x&g_*tyb z!JT5tFE_Fxc(%|%jeq<0*urq_mu0Nc^GH#T-3eKSX6`Y=$AMo^o&Gp=kvo6*y=#R9 z=URKM$m!Z}57X(`0~6X9HWjhMhbqc(jsL9EWodt(v27WTC^-G=c#o+ON8}o`g~bSZ zKNqg>Sii87Bq!Ce-KEjckiBV*QLdXm`;l)a2uUVi7}iQv+RiJGA0GZ@iw8OqaF{dzm|Q}i>< zR*Yw^PHv9DbNRm$Ov()L7$tjq`^}oyiC0q=A2&K~brE9a16ghudRH_yo^>vDKa71P zoYH;7W*M9L+(^~6*s;857Wk<5D+Th=58Y>?yMSQJ=VXQe*=R;)2WO0+xfEb-g7i&$rV1ZHYMc9mB%ng1yw#r8x z)H9HINxudTv-CF!YSfk#HkneFAC@bT`0>5t_inZs|0C*>j%@nOZ*~G-(5}rfa56|r zfFVWmVFGhfrkv6{B61@)4A;E>)P){Xhc=LVOTza0s-l3)I^myEO>I$qCt1P||K(Su zvTS)zvQKX|^u8%0&AzdzNO`BsS{FxM?UdkI&9! zpn-u`{mp|?JN!-0mp|(n)b<^mJ?PA24o!ca5{AljXj;kaC||lEz$Jg#RFTHbq)UtK zF%^o`D1GnF#P=Zo)2S)DY|w7cW_4hfAlm2BmPv|e$|SUBU`r;0-j54%D}`V4Df#z% zKO)xbJfyt2+Vf9s*{_eHeM1i!T5`x@vCnit%iJ(7!7V*$!ZMEiM~F*Uz0JCr`ypyd?DhczhzLW?q=8@>k;>* zoaXD-2aY#TBgVLgH*U>nj^`w~rF$eCwnr}=Ipf-;8Cr;=w&^m6fR`;Bi%eB_VI1Cl z3-ppp{AKsX&}LM4t>Wil2a>c&NZj0AthjO(bwy1MM)P5ILt^$@r+$v8{mz zE0f!x*qKHp#^AVX^+;<3oU5Z*LOH?KWy6rfEw}6zzGl=*0qiBuJ>?wg(A~_ft4BuN z{y@Hs4FNaRd~WXKYd(zUFs;LHup1X2skihqn4R)e$SgOiu;+UEC?}IxRmkhUOz6-}+qS&G<<9pHBBP%< zyIJQDY?n&#`MOOEk5>=(bDfHZ2M;0|A8xKVO^m12e&2AMnw$)W!#CfS+F-GrK%nnB zFSc1f_iNZ4rwcbQ(fNahTHPD|c_NpZkiJZtaD_9gvfL$4rarWy9YKi$BT~(D;-WTU;~bWU=-8x9UI8 z+*OqYnuo#EM4zA7L4ZVAX&wjQ?{qKlQCCIHl^0m^%h4|78oY#%eVQp-+TkA%b7e~M z;5@Xf(5_|eylSeLPWah1$x@jYEV84qHMiTieQ#uP`~OvRo?%U8T^rU{M_%z$3}%!L zLI4#I6(e0oDIp*rQCbv(34#>qgr11B&?S*#XyZ#j4NVY+o`5-_2+A0wDG)m25C{pq z<~yEW=iga-@Ad3;Kl?iC+De`e{Rg`Y;I?Y6d(;#uRX7K42eqZ-jPbKtcC7bNzYm@7 z5%w?QcXpJgSfhpvZqe&4520g3sQ#90abFV7MiqY4`?n@_QaqG-Cym$siWbf$B*f}8 z5k}@;9G++^8lHWKAZMStT(qR$L|ADct}l)*Slx=}M<21Ep5awE7td)}4rBa$eVh8S zsb#Vd;=m$al+eN>h-RbJiFYmE(dj)9!44R~1JhlsX*q9W{s}|yzWW%iY_gx6?NlEq(Ks(e;RKVS&KgZ(6`iiJAw0BRu>0AZxtxUS zOfS$#o)YYhE0%9vBV@cQU~=4iE(FbMr(7{4s=|L0^OU2<(6keukgxJP5Z`nxGnB8B z56P7`ax-b{YzhMiHJI;^l_Bk=hW#~+;Iv>}BL4mMZc<-mJ_W2;<0cp3LrNv9D0_Qi za8(kwfgAVK-oDDweytfI7}(j;r)uA8p9i$dNiWr!LyVeW@Pm1FCk9Bw@Vj1u6q;H4 zc!FJD_@cQ{4_lK=W3!|kn#UB>taEv7dyMc07X%3VxNf_ixvYz?>6?aiv;p_|0 z3a9*Y@Il8i`gR!jv9Uf0{fIL}58DtIkuBKn<5hS4B!u6}EqXJ8%j{((uyrJ)74+2p zqUBXznhv^UCa6dxk!#C^@}|L`svo)rD=ZvFqG59M*%Q@KV`*YOfxZOR5B zm`5krohFImVN22zu5qz0ih7*Sda&LC<9YQQ1IapVt3*IP6HK^L$LM~|}@aS z+o4o4H}?wYbO!L(8i6<*10#m3gt%bjLJm+45G3@0L)#+;Zbm*%e|!ilrF^Aj2RA=> z)j1!>mM%KgS!juN2aG%Zsn(y3yjBuyZ^2Q<5=}Tcx2~_>Xt97c6jEqg?ESp2Z-4Qr zXNdk!tJ~bs(I|CKtY%K_e}!EU;%L%+kVWgH?7Y+@TQQ*MaK!*W1~z@-0w}b@%Xtt1 zzrtZb%v`>0Bjn9{_gIsYC-7TdC@X5}p^pz8;@x-)3@mr}9kAPg7 znfdc)Gunc*;SThdL%K++HsCRJb^hM3BBN#%*sGS_Kigbg{pd{gvyaR#kqp zj(yLR*SpLoYs6~Jo{Us;W5r?YoDzj;QVjXKsvccYM+!^*@G*8@o_dkY2)@c0U`>sH z(_z?gn7ljt;bzR?q?1Urdn4<4+W905c$d3br$KsdhrL4MPR^9$vHLL3by7F4`IV80 zhg>x^h;CF`v=g-glj$pmX-Srzo~d2~wAY8AH5Oj^Hm18K&e@7p^nZ76ik1g&irR)- zans%qK_0jtpiFnoO^x)@xWzxa#1t>f6ih3lo8*2^ZYvFJ#)^d`?xCfLjyBg z#zRMWAEXHNGJ4aMG|Z)}89$!dvv8Jex?~$u0iu{Y$mg_w3M~cBRnM{zP4cpTOZ{$< zyPH+Zllp*o|D{Y5gpknHeW% z4=zXp_=AIk{zY2v?DmV#B{O;WW};yQ;CEYROSbvwM9&^U)aU5na8b?>&%;3klzu6A zMJ#)GUP?Z0cuT`No5IBJTcYuD9|I1E8EEphvub@TF8FVDJd%rl4||priWgaOCc8qn zYvy1lDA1&@W4zQ`kN1#)0Q@bOBdOEk`dB${Yw?o>SS7ePGHwWu#}|S?Rm9)e`O&}o z_^$PShp*%4y6dIQH~%~LrK-H#2n5+a1Ypv?-`W&9jntAb0%>E|Ly}3bfoW+pO-Lcx zXXOOxEL~h7?J?NX!w9Gfx7oi!$e_K4c^X&{{*G!x>u_5O4Wp5(Wa#T3LrKBp?1M?% zy1KeaG(`&0Sw(TDb-14W@CvnkuWD##x<*sGMzN{1uS=D|6c%O$cZXBZD! zBL?DPVe(3YFM;8xP6H&@C^~&b9eepGW~zF}2ifyHxAQyMToOArH#c`_5a|_XW7;y# z-w?S)M(zY9Zly@5ZW*SF+CV!|q@6rff4^eT)(j{H+E@g|rVshwYMM~g-poES_1W5i zB+Q8!Cy6_+G?6{YA@aXngsBJ%DCJvnY;~kqtgr5pqdH>_?w%@6PpS)UyZpT9@Ozln zF{~mx9xz@XfJIyHTBiNsILjt5MIdcaK-4o#+&gl@cq_%`&B~$*B0b72BxLOkm>z_) zC96S@0I$=0v-;`RP%tGzUs9Oia#=8~;;12Qc^NrCfBzn*MrHDk|K!#wRA-z3+4lG` ztm@C}yezPjLWnK+IgaMkboaaE2A*uT4hY!WUUOz@A^X{Lf)^R%RF@Sys zXeWjM21^epw8w~+;^FwsoMWyQPGO39utmikcS!}n4*TzBn1>_-cGF_pz}kvaqkN=L zF8~{sU@?lLW?!~N`<^eE2hA~f_z>tassEa=n)!Od5%moXViyc%HF$E-b z-nC@fL)o^y!f3ej!_>~VWnV9Z{>1w{U{&{#v!#%jYjhu!es0iiA+5Bl7=c9J`7bM` zpc{j1ipTSXKly-A{R*3*xR@XPGX~4_4-?yH4O_{LOm8o(=0pihB*Sl?e zjU6V;iEjWQ;;wkx;Av9TY- zRzf3}ABwUJqTI<#nepooT?Tz|1Sl&X(Axve^fvbEQwFk4e2BR_ulNEo-O zR`kyC?>wG^Id)G;Ab<7_bG)<*9%K_77I)M@gn3oj)p6BeD61AH2twVG5q zDuF#e#=tj&(PO8+fB)`}vDTz5vw^DGs}{K~DI_a!FN-IIRaP$K^ytg~4j)ST2I@Eq zJXLyH|KkP}lA&lEv5E*c18|Ulh(W%e^z4kbn5-JH!af%!Ns& z{sUfFMNrSYxskxWM6OL~=z0=Ws{CSg)_Yv#gD-5-suwmgX@EjeW&mxB{(TQ)_bnv)b>)Ji%2Za3CNmR7U5NkkK%I#Dc6@XZkI5hnfr@4!G89xHyWn8>2-!!hsc$ZRpoAgwUC&*$ogLSg(r7B$J)XA{fFBq~yr3eQ9Es~qxt2CL%UrDp$ z { + return await invoke('plugin:os|locale') +} + +/** + * Returns the host name of the operating system. + * @example + * ```typescript + * import { hostname } from '@tauri-apps/plugin-os'; + * const hostname = await hostname(); + * ``` + * + * @returns A promise resolving to the host name of the operating system. + * @since 2.0.0 + */ +async function hostname(): Promise { + return await invoke('plugin:os|hostname') +} + +export { + eol, + platform, + family, + version, + type, + arch, + locale, + exeExtension, + hostname +} +export type { Platform, OsType, Arch, Family } diff --git a/plugins/os/package.json b/plugins/os/package.json new file mode 100644 index 0000000..dbaef8e --- /dev/null +++ b/plugins/os/package.json @@ -0,0 +1,29 @@ +{ + "name": "@tauri-apps/plugin-os", + "version": "2.3.2", + "license": "MIT OR Apache-2.0", + "authors": [ + "Tauri Programme within The Commons Conservancy" + ], + "repository": "https://github.com/tauri-apps/plugins-workspace", + "type": "module", + "types": "./dist-js/index.d.ts", + "main": "./dist-js/index.cjs", + "module": "./dist-js/index.js", + "exports": { + "types": "./dist-js/index.d.ts", + "import": "./dist-js/index.js", + "require": "./dist-js/index.cjs" + }, + "scripts": { + "build": "rollup -c" + }, + "files": [ + "dist-js", + "README.md", + "LICENSE" + ], + "dependencies": { + "@tauri-apps/api": "^2.11.0" + } +} diff --git a/plugins/os/permissions/autogenerated/commands/arch.toml b/plugins/os/permissions/autogenerated/commands/arch.toml new file mode 100644 index 0000000..160cce0 --- /dev/null +++ b/plugins/os/permissions/autogenerated/commands/arch.toml @@ -0,0 +1,13 @@ +# Automatically generated - DO NOT EDIT! + +"$schema" = "../../schemas/schema.json" + +[[permission]] +identifier = "allow-arch" +description = "Enables the arch command without any pre-configured scope." +commands.allow = ["arch"] + +[[permission]] +identifier = "deny-arch" +description = "Denies the arch command without any pre-configured scope." +commands.deny = ["arch"] diff --git a/plugins/os/permissions/autogenerated/commands/exe_extension.toml b/plugins/os/permissions/autogenerated/commands/exe_extension.toml new file mode 100644 index 0000000..aef4d2e --- /dev/null +++ b/plugins/os/permissions/autogenerated/commands/exe_extension.toml @@ -0,0 +1,13 @@ +# Automatically generated - DO NOT EDIT! + +"$schema" = "../../schemas/schema.json" + +[[permission]] +identifier = "allow-exe-extension" +description = "Enables the exe_extension command without any pre-configured scope." +commands.allow = ["exe_extension"] + +[[permission]] +identifier = "deny-exe-extension" +description = "Denies the exe_extension command without any pre-configured scope." +commands.deny = ["exe_extension"] diff --git a/plugins/os/permissions/autogenerated/commands/family.toml b/plugins/os/permissions/autogenerated/commands/family.toml new file mode 100644 index 0000000..4109be3 --- /dev/null +++ b/plugins/os/permissions/autogenerated/commands/family.toml @@ -0,0 +1,13 @@ +# Automatically generated - DO NOT EDIT! + +"$schema" = "../../schemas/schema.json" + +[[permission]] +identifier = "allow-family" +description = "Enables the family command without any pre-configured scope." +commands.allow = ["family"] + +[[permission]] +identifier = "deny-family" +description = "Denies the family command without any pre-configured scope." +commands.deny = ["family"] diff --git a/plugins/os/permissions/autogenerated/commands/hostname.toml b/plugins/os/permissions/autogenerated/commands/hostname.toml new file mode 100644 index 0000000..c65376c --- /dev/null +++ b/plugins/os/permissions/autogenerated/commands/hostname.toml @@ -0,0 +1,13 @@ +# Automatically generated - DO NOT EDIT! + +"$schema" = "../../schemas/schema.json" + +[[permission]] +identifier = "allow-hostname" +description = "Enables the hostname command without any pre-configured scope." +commands.allow = ["hostname"] + +[[permission]] +identifier = "deny-hostname" +description = "Denies the hostname command without any pre-configured scope." +commands.deny = ["hostname"] diff --git a/plugins/os/permissions/autogenerated/commands/locale.toml b/plugins/os/permissions/autogenerated/commands/locale.toml new file mode 100644 index 0000000..2a85e47 --- /dev/null +++ b/plugins/os/permissions/autogenerated/commands/locale.toml @@ -0,0 +1,13 @@ +# Automatically generated - DO NOT EDIT! + +"$schema" = "../../schemas/schema.json" + +[[permission]] +identifier = "allow-locale" +description = "Enables the locale command without any pre-configured scope." +commands.allow = ["locale"] + +[[permission]] +identifier = "deny-locale" +description = "Denies the locale command without any pre-configured scope." +commands.deny = ["locale"] diff --git a/plugins/os/permissions/autogenerated/commands/os_type.toml b/plugins/os/permissions/autogenerated/commands/os_type.toml new file mode 100644 index 0000000..c91987a --- /dev/null +++ b/plugins/os/permissions/autogenerated/commands/os_type.toml @@ -0,0 +1,13 @@ +# Automatically generated - DO NOT EDIT! + +"$schema" = "../../schemas/schema.json" + +[[permission]] +identifier = "allow-os-type" +description = "Enables the os_type command without any pre-configured scope." +commands.allow = ["os_type"] + +[[permission]] +identifier = "deny-os-type" +description = "Denies the os_type command without any pre-configured scope." +commands.deny = ["os_type"] diff --git a/plugins/os/permissions/autogenerated/commands/platform.toml b/plugins/os/permissions/autogenerated/commands/platform.toml new file mode 100644 index 0000000..49dc08d --- /dev/null +++ b/plugins/os/permissions/autogenerated/commands/platform.toml @@ -0,0 +1,13 @@ +# Automatically generated - DO NOT EDIT! + +"$schema" = "../../schemas/schema.json" + +[[permission]] +identifier = "allow-platform" +description = "Enables the platform command without any pre-configured scope." +commands.allow = ["platform"] + +[[permission]] +identifier = "deny-platform" +description = "Denies the platform command without any pre-configured scope." +commands.deny = ["platform"] diff --git a/plugins/os/permissions/autogenerated/commands/version.toml b/plugins/os/permissions/autogenerated/commands/version.toml new file mode 100644 index 0000000..2d65e32 --- /dev/null +++ b/plugins/os/permissions/autogenerated/commands/version.toml @@ -0,0 +1,13 @@ +# Automatically generated - DO NOT EDIT! + +"$schema" = "../../schemas/schema.json" + +[[permission]] +identifier = "allow-version" +description = "Enables the version command without any pre-configured scope." +commands.allow = ["version"] + +[[permission]] +identifier = "deny-version" +description = "Denies the version command without any pre-configured scope." +commands.deny = ["version"] diff --git a/plugins/os/permissions/autogenerated/reference.md b/plugins/os/permissions/autogenerated/reference.md new file mode 100644 index 0000000..7ac7375 --- /dev/null +++ b/plugins/os/permissions/autogenerated/reference.md @@ -0,0 +1,237 @@ +## Default Permission + +This permission set configures which +operating system information are available +to gather from the frontend. + +#### Granted Permissions + +All information except the host name are available. + +#### This default permission set includes the following: + +- `allow-arch` +- `allow-exe-extension` +- `allow-family` +- `allow-locale` +- `allow-os-type` +- `allow-platform` +- `allow-version` + +## Permission Table + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
IdentifierDescription
+ +`os:allow-arch` + + + +Enables the arch command without any pre-configured scope. + +
+ +`os:deny-arch` + + + +Denies the arch command without any pre-configured scope. + +
+ +`os:allow-exe-extension` + + + +Enables the exe_extension command without any pre-configured scope. + +
+ +`os:deny-exe-extension` + + + +Denies the exe_extension command without any pre-configured scope. + +
+ +`os:allow-family` + + + +Enables the family command without any pre-configured scope. + +
+ +`os:deny-family` + + + +Denies the family command without any pre-configured scope. + +
+ +`os:allow-hostname` + + + +Enables the hostname command without any pre-configured scope. + +
+ +`os:deny-hostname` + + + +Denies the hostname command without any pre-configured scope. + +
+ +`os:allow-locale` + + + +Enables the locale command without any pre-configured scope. + +
+ +`os:deny-locale` + + + +Denies the locale command without any pre-configured scope. + +
+ +`os:allow-os-type` + + + +Enables the os_type command without any pre-configured scope. + +
+ +`os:deny-os-type` + + + +Denies the os_type command without any pre-configured scope. + +
+ +`os:allow-platform` + + + +Enables the platform command without any pre-configured scope. + +
+ +`os:deny-platform` + + + +Denies the platform command without any pre-configured scope. + +
+ +`os:allow-version` + + + +Enables the version command without any pre-configured scope. + +
+ +`os:deny-version` + + + +Denies the version command without any pre-configured scope. + +
diff --git a/plugins/os/permissions/default.toml b/plugins/os/permissions/default.toml new file mode 100644 index 0000000..217b389 --- /dev/null +++ b/plugins/os/permissions/default.toml @@ -0,0 +1,23 @@ +"$schema" = "schemas/schema.json" + +[default] +description = """ +This permission set configures which +operating system information are available +to gather from the frontend. + +#### Granted Permissions + +All information except the host name are available. + +""" + +permissions = [ + "allow-arch", + "allow-exe-extension", + "allow-family", + "allow-locale", + "allow-os-type", + "allow-platform", + "allow-version", +] diff --git a/plugins/os/permissions/schemas/schema.json b/plugins/os/permissions/schemas/schema.json new file mode 100644 index 0000000..36680b4 --- /dev/null +++ b/plugins/os/permissions/schemas/schema.json @@ -0,0 +1,402 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "PermissionFile", + "description": "Permission file that can define a default permission, a set of permissions or a list of inlined permissions.", + "type": "object", + "properties": { + "default": { + "description": "The default permission set for the plugin", + "anyOf": [ + { + "$ref": "#/definitions/DefaultPermission" + }, + { + "type": "null" + } + ] + }, + "set": { + "description": "A list of permissions sets defined", + "type": "array", + "items": { + "$ref": "#/definitions/PermissionSet" + } + }, + "permission": { + "description": "A list of inlined permissions", + "default": [], + "type": "array", + "items": { + "$ref": "#/definitions/Permission" + } + } + }, + "definitions": { + "DefaultPermission": { + "description": "The default permission set of the plugin.\n\nWorks similarly to a permission with the \"default\" identifier.", + "type": "object", + "required": [ + "permissions" + ], + "properties": { + "version": { + "description": "The version of the permission.", + "type": [ + "integer", + "null" + ], + "format": "uint64", + "minimum": 1.0 + }, + "description": { + "description": "Human-readable description of what the permission does. Tauri convention is to use `

` headings in markdown content for Tauri documentation generation purposes.", + "type": [ + "string", + "null" + ] + }, + "permissions": { + "description": "All permissions this set contains.", + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "PermissionSet": { + "description": "A set of direct permissions grouped together under a new name.", + "type": "object", + "required": [ + "description", + "identifier", + "permissions" + ], + "properties": { + "identifier": { + "description": "A unique identifier for the permission.", + "type": "string" + }, + "description": { + "description": "Human-readable description of what the permission does.", + "type": "string" + }, + "permissions": { + "description": "All permissions this set contains.", + "type": "array", + "items": { + "$ref": "#/definitions/PermissionKind" + } + } + } + }, + "Permission": { + "description": "Descriptions of explicit privileges of commands.\n\nIt can enable commands to be accessible in the frontend of the application.\n\nIf the scope is defined it can be used to fine grain control the access of individual or multiple commands.", + "type": "object", + "required": [ + "identifier" + ], + "properties": { + "version": { + "description": "The version of the permission.", + "type": [ + "integer", + "null" + ], + "format": "uint64", + "minimum": 1.0 + }, + "identifier": { + "description": "A unique identifier for the permission.", + "type": "string" + }, + "description": { + "description": "Human-readable description of what the permission does. Tauri internal convention is to use `

` headings in markdown content for Tauri documentation generation purposes.", + "type": [ + "string", + "null" + ] + }, + "commands": { + "description": "Allowed or denied commands when using this permission.", + "default": { + "allow": [], + "deny": [] + }, + "allOf": [ + { + "$ref": "#/definitions/Commands" + } + ] + }, + "scope": { + "description": "Allowed or denied scoped when using this permission.", + "allOf": [ + { + "$ref": "#/definitions/Scopes" + } + ] + }, + "platforms": { + "description": "Target platforms this permission applies. By default all platforms are affected by this permission.", + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/Target" + } + } + } + }, + "Commands": { + "description": "Allowed and denied commands inside a permission.\n\nIf two commands clash inside of `allow` and `deny`, it should be denied by default.", + "type": "object", + "properties": { + "allow": { + "description": "Allowed command.", + "default": [], + "type": "array", + "items": { + "type": "string" + } + }, + "deny": { + "description": "Denied command, which takes priority.", + "default": [], + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "Scopes": { + "description": "An argument for fine grained behavior control of Tauri commands.\n\nIt can be of any serde serializable type and is used to allow or prevent certain actions inside a Tauri command. The configured scope is passed to the command and will be enforced by the command implementation.\n\n## Example\n\n```json { \"allow\": [{ \"path\": \"$HOME/**\" }], \"deny\": [{ \"path\": \"$HOME/secret.txt\" }] } ```", + "type": "object", + "properties": { + "allow": { + "description": "Data that defines what is allowed by the scope.", + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/Value" + } + }, + "deny": { + "description": "Data that defines what is denied by the scope. This should be prioritized by validation logic.", + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/Value" + } + } + } + }, + "Value": { + "description": "All supported ACL values.", + "anyOf": [ + { + "description": "Represents a null JSON value.", + "type": "null" + }, + { + "description": "Represents a [`bool`].", + "type": "boolean" + }, + { + "description": "Represents a valid ACL [`Number`].", + "allOf": [ + { + "$ref": "#/definitions/Number" + } + ] + }, + { + "description": "Represents a [`String`].", + "type": "string" + }, + { + "description": "Represents a list of other [`Value`]s.", + "type": "array", + "items": { + "$ref": "#/definitions/Value" + } + }, + { + "description": "Represents a map of [`String`] keys to [`Value`]s.", + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/Value" + } + } + ] + }, + "Number": { + "description": "A valid ACL number.", + "anyOf": [ + { + "description": "Represents an [`i64`].", + "type": "integer", + "format": "int64" + }, + { + "description": "Represents a [`f64`].", + "type": "number", + "format": "double" + } + ] + }, + "Target": { + "description": "Platform target.", + "oneOf": [ + { + "description": "MacOS.", + "type": "string", + "enum": [ + "macOS" + ] + }, + { + "description": "Windows.", + "type": "string", + "enum": [ + "windows" + ] + }, + { + "description": "Linux.", + "type": "string", + "enum": [ + "linux" + ] + }, + { + "description": "Android.", + "type": "string", + "enum": [ + "android" + ] + }, + { + "description": "iOS.", + "type": "string", + "enum": [ + "iOS" + ] + } + ] + }, + "PermissionKind": { + "type": "string", + "oneOf": [ + { + "description": "Enables the arch command without any pre-configured scope.", + "type": "string", + "const": "allow-arch", + "markdownDescription": "Enables the arch command without any pre-configured scope." + }, + { + "description": "Denies the arch command without any pre-configured scope.", + "type": "string", + "const": "deny-arch", + "markdownDescription": "Denies the arch command without any pre-configured scope." + }, + { + "description": "Enables the exe_extension command without any pre-configured scope.", + "type": "string", + "const": "allow-exe-extension", + "markdownDescription": "Enables the exe_extension command without any pre-configured scope." + }, + { + "description": "Denies the exe_extension command without any pre-configured scope.", + "type": "string", + "const": "deny-exe-extension", + "markdownDescription": "Denies the exe_extension command without any pre-configured scope." + }, + { + "description": "Enables the family command without any pre-configured scope.", + "type": "string", + "const": "allow-family", + "markdownDescription": "Enables the family command without any pre-configured scope." + }, + { + "description": "Denies the family command without any pre-configured scope.", + "type": "string", + "const": "deny-family", + "markdownDescription": "Denies the family command without any pre-configured scope." + }, + { + "description": "Enables the hostname command without any pre-configured scope.", + "type": "string", + "const": "allow-hostname", + "markdownDescription": "Enables the hostname command without any pre-configured scope." + }, + { + "description": "Denies the hostname command without any pre-configured scope.", + "type": "string", + "const": "deny-hostname", + "markdownDescription": "Denies the hostname command without any pre-configured scope." + }, + { + "description": "Enables the locale command without any pre-configured scope.", + "type": "string", + "const": "allow-locale", + "markdownDescription": "Enables the locale command without any pre-configured scope." + }, + { + "description": "Denies the locale command without any pre-configured scope.", + "type": "string", + "const": "deny-locale", + "markdownDescription": "Denies the locale command without any pre-configured scope." + }, + { + "description": "Enables the os_type command without any pre-configured scope.", + "type": "string", + "const": "allow-os-type", + "markdownDescription": "Enables the os_type command without any pre-configured scope." + }, + { + "description": "Denies the os_type command without any pre-configured scope.", + "type": "string", + "const": "deny-os-type", + "markdownDescription": "Denies the os_type command without any pre-configured scope." + }, + { + "description": "Enables the platform command without any pre-configured scope.", + "type": "string", + "const": "allow-platform", + "markdownDescription": "Enables the platform command without any pre-configured scope." + }, + { + "description": "Denies the platform command without any pre-configured scope.", + "type": "string", + "const": "deny-platform", + "markdownDescription": "Denies the platform command without any pre-configured scope." + }, + { + "description": "Enables the version command without any pre-configured scope.", + "type": "string", + "const": "allow-version", + "markdownDescription": "Enables the version command without any pre-configured scope." + }, + { + "description": "Denies the version command without any pre-configured scope.", + "type": "string", + "const": "deny-version", + "markdownDescription": "Denies the version command without any pre-configured scope." + }, + { + "description": "This permission set configures which\noperating system information are available\nto gather from the frontend.\n\n#### Granted Permissions\n\nAll information except the host name are available.\n\n\n#### This default permission set includes:\n\n- `allow-arch`\n- `allow-exe-extension`\n- `allow-family`\n- `allow-locale`\n- `allow-os-type`\n- `allow-platform`\n- `allow-version`", + "type": "string", + "const": "default", + "markdownDescription": "This permission set configures which\noperating system information are available\nto gather from the frontend.\n\n#### Granted Permissions\n\nAll information except the host name are available.\n\n\n#### This default permission set includes:\n\n- `allow-arch`\n- `allow-exe-extension`\n- `allow-family`\n- `allow-locale`\n- `allow-os-type`\n- `allow-platform`\n- `allow-version`" + } + ] + } + } +} \ No newline at end of file diff --git a/plugins/os/rollup.config.js b/plugins/os/rollup.config.js new file mode 100644 index 0000000..1f349ec --- /dev/null +++ b/plugins/os/rollup.config.js @@ -0,0 +1,7 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +import { createConfig } from '../../shared/rollup.config.js' + +export default createConfig() diff --git a/plugins/os/src/commands.rs b/plugins/os/src/commands.rs new file mode 100644 index 0000000..b10c7f5 --- /dev/null +++ b/plugins/os/src/commands.rs @@ -0,0 +1,13 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +#[tauri::command] +pub fn locale() -> Option { + crate::locale() +} + +#[tauri::command] +pub fn hostname() -> String { + crate::hostname() +} diff --git a/plugins/os/src/error.rs b/plugins/os/src/error.rs new file mode 100644 index 0000000..7c752ba --- /dev/null +++ b/plugins/os/src/error.rs @@ -0,0 +1,20 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use serde::{Serialize, Serializer}; + +/// All errors that can occur while running the os plugin. +/// +/// This enum currently has no variants: none of the plugin's commands can fail. +#[derive(Debug, thiserror::Error)] +pub enum Error {} + +impl Serialize for Error { + fn serialize(&self, serializer: S) -> std::result::Result + where + S: Serializer, + { + serializer.serialize_str(self.to_string().as_ref()) + } +} diff --git a/plugins/os/src/init.js b/plugins/os/src/init.js new file mode 100644 index 0000000..97eeab3 --- /dev/null +++ b/plugins/os/src/init.js @@ -0,0 +1,16 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +// eslint-disable-next-line +Object.defineProperty(window, '__TAURI_OS_PLUGIN_INTERNALS__', { + value: { + eol: __TEMPLATE_eol__, + os_type: __TEMPLATE_os_type__, + platform: __TEMPLATE_platform__, + family: __TEMPLATE_family__, + version: __TEMPLATE_version__, + arch: __TEMPLATE_arch__, + exe_extension: __TEMPLATE_exe_extension__ + } +}) diff --git a/plugins/os/src/lib.rs b/plugins/os/src/lib.rs new file mode 100644 index 0000000..853dbdf --- /dev/null +++ b/plugins/os/src/lib.rs @@ -0,0 +1,150 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +//! Read information about the operating system. + +#![doc( + html_logo_url = "https://github.com/tauri-apps/tauri/raw/dev/app-icon.png", + html_favicon_url = "https://github.com/tauri-apps/tauri/raw/dev/app-icon.png" +)] + +use std::fmt::Display; + +pub use os_info::Version; +use serialize_to_javascript::{default_template, DefaultTemplate, Template}; +use tauri::{ + plugin::{Builder, TauriPlugin}, + Runtime, +}; + +mod commands; +mod error; + +pub use error::Error; + +/// The type of the current operating system, as returned by [`type_`]. +pub enum OsType { + /// Linux and Linux-based systems such as FreeBSD, DragonFly BSD, NetBSD and OpenBSD. + Linux, + /// Windows. + Windows, + /// macOS. + Macos, + /// iOS. + IOS, + /// Android. + Android, +} + +impl Display for OsType { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Linux => write!(f, "linux"), + Self::Windows => write!(f, "windows"), + Self::Macos => write!(f, "macos"), + Self::IOS => write!(f, "ios"), + Self::Android => write!(f, "android"), + } + } +} + +/// Returns a string describing the specific operating system in use, see [std::env::consts::OS]. +pub fn platform() -> &'static str { + std::env::consts::OS +} + +/// Returns the current operating system version. +pub fn version() -> Version { + os_info::get().version().clone() +} + +/// Returns the current operating system type. +pub fn type_() -> OsType { + #[cfg(any( + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd" + ))] + return OsType::Linux; + #[cfg(target_os = "windows")] + return OsType::Windows; + #[cfg(target_os = "macos")] + return OsType::Macos; + #[cfg(target_os = "ios")] + return OsType::IOS; + #[cfg(target_os = "android")] + return OsType::Android; +} + +/// Returns the current operating system family, see [std::env::consts::FAMILY]. +pub fn family() -> &'static str { + std::env::consts::FAMILY +} + +/// Returns the current operating system architecture, see [std::env::consts::ARCH]. +pub fn arch() -> &'static str { + std::env::consts::ARCH +} + +/// Returns the file extension, if any, used for executable binaries on this platform. Example value is `exe`, see [std::env::consts::EXE_EXTENSION]. +pub fn exe_extension() -> &'static str { + std::env::consts::EXE_EXTENSION +} + +/// Returns the current operating system locale with the `BCP-47` language tag. If the locale couldn't be obtained, `None` is returned instead. +pub fn locale() -> Option { + sys_locale::get_locale() +} + +/// Returns the current operating system hostname. +pub fn hostname() -> String { + gethostname::gethostname().to_string_lossy().to_string() +} + +#[derive(Template)] +#[default_template("./init.js")] +struct InitJavascript<'a> { + eol: &'static str, + os_type: String, + platform: &'a str, + family: &'a str, + version: String, + arch: &'a str, + exe_extension: &'a str, +} + +impl InitJavascript<'_> { + fn new() -> Self { + Self { + #[cfg(windows)] + eol: "\r\n", + #[cfg(not(windows))] + eol: "\n", + os_type: crate::type_().to_string(), + platform: crate::platform(), + family: crate::family(), + version: crate::version().to_string(), + arch: crate::arch(), + exe_extension: crate::exe_extension(), + } + } +} + +/// Initializes the plugin. +pub fn init() -> TauriPlugin { + let init_js = InitJavascript::new() + .render_default(&Default::default()) + // this will never fail with the above global_os_api values + .unwrap(); + + Builder::new("os") + .js_init_script(init_js.to_string()) + .invoke_handler(tauri::generate_handler![ + commands::locale, + commands::hostname + ]) + .build() +} diff --git a/plugins/os/tsconfig.json b/plugins/os/tsconfig.json new file mode 100644 index 0000000..4374468 --- /dev/null +++ b/plugins/os/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "rootDir": "guest-js" + }, + "include": ["guest-js/*.ts"] +} diff --git a/plugins/persisted-scope/CHANGELOG.md b/plugins/persisted-scope/CHANGELOG.md new file mode 100644 index 0000000..3dccf26 --- /dev/null +++ b/plugins/persisted-scope/CHANGELOG.md @@ -0,0 +1,350 @@ +# Changelog + +## [2.3.8] + +- [`f8053e65`](https://github.com/tauri-apps/plugins-workspace/commit/f8053e659e4ccd85c1f52833411ff8417cbc5e69) ([#3527](https://github.com/tauri-apps/plugins-workspace/pull/3527) by [@Legend-Master](https://github.com/tauri-apps/plugins-workspace/../../Legend-Master)) Documented Cargo feature flags in each plugin's crate-level documentation. + +### Dependencies + +- Upgraded to `fs@2.5.2` + +## \[2.3.7] + +### Dependencies + +- Upgraded to `fs@2.5.1` + +## \[2.3.6] + +### Dependencies + +- Upgraded to `fs@2.5.0` + +## \[2.3.5] + +### Dependencies + +- Upgraded to `fs@2.4.5` + +## \[2.3.4] + +- [`93426f85`](https://github.com/tauri-apps/plugins-workspace/commit/93426f85120f49beb9f40222bff45185a32d54a9) Fixed an issue that caused docs.rs builds to fail. No user facing changes. + +### Dependencies + +- Upgraded to `fs@2.4.4` + +## \[2.3.3] + +### Dependencies + +- Upgraded to `fs@2.4.3` + +## \[2.3.2] + +### Dependencies + +- Upgraded to `fs@2.4.2` + +## \[2.3.1] + +### Dependencies + +- Upgraded to `fs@2.4.1` + +## \[2.3.0] + +- [`f209b2f2`](https://github.com/tauri-apps/plugins-workspace/commit/f209b2f23cb29133c97ad5961fb46ef794dbe063) ([#2804](https://github.com/tauri-apps/plugins-workspace/pull/2804) by [@renovate](https://github.com/tauri-apps/plugins-workspace/../../renovate)) Updated tauri to 2.6 + +### Dependencies + +- Upgraded to `fs@2.4.0` + +## \[2.2.2] + +### Dependencies + +- Upgraded to `fs@2.3.0` + +## \[2.2.1] + +### Dependencies + +- Upgraded to `fs@2.2.1` + +## \[2.2.0] + +- [`3a79266b`](https://github.com/tauri-apps/plugins-workspace/commit/3a79266b8cf96a55b1ae6339d725567d45a44b1d) ([#2173](https://github.com/tauri-apps/plugins-workspace/pull/2173) by [@FabianLars](https://github.com/tauri-apps/plugins-workspace/../../FabianLars)) Bumped all plugins to `v2.2.0`. From now, the versions for the Rust and JavaScript packages of each plugin will be in sync with each other. + +### Dependencies + +- Upgraded to `fs@2.2.0` + +## \[2.1.1] + +### Dependencies + +- Upgraded to `fs@2.1.1` + +## \[2.1.0] + +- [`fecfd553`](https://github.com/tauri-apps/plugins-workspace/commit/fecfd5533a6452f054fbcd909021f12b0dce834f) ([#2070](https://github.com/tauri-apps/plugins-workspace/pull/2070) by [@FabianLars](https://github.com/tauri-apps/plugins-workspace/../../FabianLars)) **Breaking Change:** Replaced the custom `tauri_plugin_fs::Scope` struct with `tauri::fs::Scope`. + +### Dependencies + +- Upgraded to `fs@2.1.0` + +## \[2.0.3] + +### Dependencies + +- Upgraded to `fs@2.0.3` + +## \[2.0.2] + +### Dependencies + +- Upgraded to `fs@2.0.2` + +## \[2.0.1] + +- [`a1a82208`](https://github.com/tauri-apps/plugins-workspace/commit/a1a82208ed4ab87f83310be0dc95428aec9ab241) ([#1873](https://github.com/tauri-apps/plugins-workspace/pull/1873) by [@lucasfernog](https://github.com/tauri-apps/plugins-workspace/../../lucasfernog)) Downgrade MSRV to 1.77.2 to support Windows 7. + +### Dependencies + +- Upgraded to `fs@2.0.1` + +## \[2.0.0] + +- [`e2c4dfb6`](https://github.com/tauri-apps/plugins-workspace/commit/e2c4dfb6af43e5dd8d9ceba232c315f5febd55c1) Update to tauri v2 stable release. + +### Dependencies + +- Upgraded to `fs@2.0.0` + +## \[2.0.0-rc.6] + +### Dependencies + +- Upgraded to `fs@2.0.0-rc.6` + +## \[2.0.0-rc.5] + +### Dependencies + +- Upgraded to `fs@2.0.0-rc.5` + +## \[2.0.0-rc.4] + +### Dependencies + +- Upgraded to `fs@2.0.0-rc.4` + +## \[2.0.0-rc.3] + +### Dependencies + +- Upgraded to `fs@2.0.0-rc.3` + +## \[2.0.0-rc.2] + +### Dependencies + +- Upgraded to `fs@2.0.0-rc.2` + +## \[2.0.0-rc.1] + +- [`e2e97db5`](https://github.com/tauri-apps/plugins-workspace/commit/e2e97db51983267f5be84d4f6f0278d58834d1f5) ([#1701](https://github.com/tauri-apps/plugins-workspace/pull/1701) by [@lucasfernog](https://github.com/tauri-apps/plugins-workspace/../../lucasfernog)) Update to tauri 2.0.0-rc.8 + +### Dependencies + +- Upgraded to `fs@2.0.0-rc.1` + +## \[2.0.0-rc.0] + +- [`9887d1`](https://github.com/tauri-apps/plugins-workspace/commit/9887d14bd0e971c4c0f5c1188fc4005d3fc2e29e) Update to tauri RC. + +### Dependencies + +- Upgraded to `fs@2.0.0-rc.0` + +## \[2.0.0-beta.12] + +- [`e847cedc`](https://github.com/tauri-apps/plugins-workspace/commit/e847cedc1f46f3e7a2ad81ea579b620bc5b992d7) ([#1402](https://github.com/tauri-apps/plugins-workspace/pull/1402) by [@Legend-Master](https://github.com/tauri-apps/plugins-workspace/../../Legend-Master)) Use no default features on tauri for all plugins so that consumers can use `default-features = false` on tauri, note that this will still enable wry feature on iOS +- [`6de87966`](https://github.com/tauri-apps/plugins-workspace/commit/6de87966ecc00ad9d91c25be452f1f46bd2b7e1f) ([#1597](https://github.com/tauri-apps/plugins-workspace/pull/1597) by [@Legend-Master](https://github.com/tauri-apps/plugins-workspace/../../Legend-Master)) Update to tauri beta.25. + +### Dependencies + +- Upgraded to `fs@2.0.0-beta.12` + +## \[2.0.0-beta.11] + +- [`22a17980`](https://github.com/tauri-apps/plugins-workspace/commit/22a17980ff4f6f8c40adb1b8f4ffc6dae2fe7e30) ([#1537](https://github.com/tauri-apps/plugins-workspace/pull/1537) by [@lucasfernog](https://github.com/tauri-apps/plugins-workspace/../../lucasfernog)) Update to tauri beta.24. + +### Dependencies + +- Upgraded to `fs@2.0.0-beta.11` + +## \[2.0.0-beta.10] + +- [`76daee7a`](https://github.com/tauri-apps/plugins-workspace/commit/76daee7aafece34de3092c86e531cf9eb1138989) ([#1512](https://github.com/tauri-apps/plugins-workspace/pull/1512) by [@renovate](https://github.com/tauri-apps/plugins-workspace/../../renovate)) Update to tauri beta.23. + +### Dependencies + +- Upgraded to `fs@2.0.0-beta.10` + +## \[2.0.0-beta.9] + +- [`9013854f`](https://github.com/tauri-apps/plugins-workspace/commit/9013854f42a49a230b9dbb9d02774765528a923f)([#1382](https://github.com/tauri-apps/plugins-workspace/pull/1382)) Update to tauri beta.22. + +### Dependencies + +- Upgraded to `fs@2.0.0-beta.9` + +## \[2.0.0-beta.8] + +- [`430bd6f4`](https://github.com/tauri-apps/plugins-workspace/commit/430bd6f4f379bee5d232ae6b098ae131db7f178a)([#1363](https://github.com/tauri-apps/plugins-workspace/pull/1363)) Update to tauri beta.20. + +### Dependencies + +- Upgraded to `fs@2.0.0-beta.8` + +## \[2.0.0-beta.7] + +- [`bd1ed590`](https://github.com/tauri-apps/plugins-workspace/commit/bd1ed5903ffcce5500310dac1e59e8c67674ef1e)([#1237](https://github.com/tauri-apps/plugins-workspace/pull/1237)) Update to tauri beta.17. + +### Dependencies + +- Upgraded to `fs@2.0.0-beta.7` + +## \[2.0.0-beta.6] + +### Dependencies + +- Upgraded to `fs@2.0.0-beta.6` + +## \[2.0.0-beta.5] + +### Dependencies + +- Upgraded to `fs@2.0.0-beta.5` + +## \[2.0.0-beta.4] + +### Dependencies + +- Upgraded to `fs@2.0.0-beta.4` + +## \[2.0.0-beta.3] + +- [`a04ea2f`](https://github.com/tauri-apps/plugins-workspace/commit/a04ea2f38294d5a3987578283badc8eec87a7752)([#1071](https://github.com/tauri-apps/plugins-workspace/pull/1071)) The global API script is now only added to the binary when the `withGlobalTauri` config is true. + +### Dependencies + +- Upgraded to `fs@2.0.0-beta.3` + +## \[2.0.0-beta.2] + +- [`99bea25`](https://github.com/tauri-apps/plugins-workspace/commit/99bea2559c2c0648c2519c50a18cd124dacef57b)([#1005](https://github.com/tauri-apps/plugins-workspace/pull/1005)) Update to tauri beta.8. + +### Dependencies + +- Upgraded to `fs@2.0.0-beta.2` + +## \[2.0.0-beta.1] + +- [`14f381a`](https://github.com/tauri-apps/plugins-workspace/commit/14f381acf8fe690acecc676922c6f05939b95734) Update MSRV to 1.75. +- [`569defb`](https://github.com/tauri-apps/plugins-workspace/commit/569defbe9492e38938554bb7bdc1be9151456d21) Update to tauri beta.4. + +### Dependencies + +- Upgraded to `fs@2.0.0-beta.1` + +## \[2.0.0-beta.0] + +- [`d198c01`](https://github.com/tauri-apps/plugins-workspace/commit/d198c014863ee260cb0de88a14b7fc4356ef7474)([#862](https://github.com/tauri-apps/plugins-workspace/pull/862)) Update to tauri beta. +- [`d198c01`](https://github.com/tauri-apps/plugins-workspace/commit/d198c014863ee260cb0de88a14b7fc4356ef7474)([#862](https://github.com/tauri-apps/plugins-workspace/pull/862)) Add permissions. + +### Dependencies + +- Upgraded to `fs@2.0.0-beta.0` + +## \[2.0.0-alpha.7] + +### Dependencies + +- Upgraded to `fs@2.0.0-alpha.7` + +## \[2.0.0-alpha.6] + +- [`2cf8faa`](https://github.com/tauri-apps/plugins-workspace/commit/2cf8faa3e149af55eb86e5aba8ebfc54210ca703)([#839](https://github.com/tauri-apps/plugins-workspace/pull/839)) Update to tauri@alpha.20. +- [`10b8039`](https://github.com/tauri-apps/plugins-workspace/commit/10b80391fcdef28e26124505053fb3a4c4f85e75)([#825](https://github.com/tauri-apps/plugins-workspace/pull/825)) Use `tauri::scope::fs::Scope` instead of local copy of its implementation. + +### Dependencies + +- Upgraded to `fs@2.0.0-alpha.6` + +## \[2.0.0-alpha.5] + +- [`387c2f9`](https://github.com/tauri-apps/plugins-workspace/commit/387c2f9e0ce4c75c07ffa3fd76391a25b58f5daf)([#802](https://github.com/tauri-apps/plugins-workspace/pull/802)) Update to tauri@alpha.18. + +### Dependencies + +- Upgraded to `fs@2.0.0-alpha.5` + +## \[2.0.0-alpha.4] + +- [`e438e0a`](https://github.com/tauri-apps/plugins-workspace/commit/e438e0a62d4b430a5159f05f13ecd397dd891a0d)([#676](https://github.com/tauri-apps/plugins-workspace/pull/676)) Update to tauri@alpha.17. + +### Dependencies + +- Upgraded to `fs@2.0.0-alpha.4` + +## \[2.0.0-alpha.3] + +- [`5c13736`](https://github.com/tauri-apps/plugins-workspace/commit/5c137365c60790e8d4037d449e8237aa3fffdab0)([#673](https://github.com/tauri-apps/plugins-workspace/pull/673)) Update MSRV to 1.75. +- [`5de23e7`](https://github.com/tauri-apps/plugins-workspace/commit/5de23e79f9880921b62e4b7a8819bc0dbc833216)([#649](https://github.com/tauri-apps/plugins-workspace/pull/649)) Update to tauri@alpha.15. + +### Dependencies + +- Upgraded to `fs@2.0.0-alpha.3` + +## \[2.0.0-alpha.2] + +- [`4e2cef9`](https://github.com/tauri-apps/plugins-workspace/commit/4e2cef9b702bbbb9cf4ee17de50791cb21f1b2a4)([#593](https://github.com/tauri-apps/plugins-workspace/pull/593)) Update to alpha.12. + +### Dependencies + +- Upgraded to `fs@2.0.0-alpha.2` + +## \[2.0.0-alpha.1] + +- [`d5a7c77`](https://github.com/tauri-apps/plugins-workspace/commit/d5a7c77a8d0e7912a6b07b22ed329004edd6e80b)([#545](https://github.com/tauri-apps/plugins-workspace/pull/545)) Fixes docs.rs build by enabling the `tauri/dox` feature flag. +- [`aba07c2`](https://github.com/tauri-apps/plugins-workspace/commit/aba07c27b887c1cc54026024227cb3f74c91e21a)([#468](https://github.com/tauri-apps/plugins-workspace/pull/468)) Split up fs and asset scopes. **This will reset the asset protocol scope once!** +- [`aba07c2`](https://github.com/tauri-apps/plugins-workspace/commit/aba07c27b887c1cc54026024227cb3f74c91e21a)([#468](https://github.com/tauri-apps/plugins-workspace/pull/468)) Fix usage of directory patterns by removing glob asterisks at the end before allowing/forbidding them. This was causing them to be escaped, and so undesirable paths were allowed/forbidden while polluting the `.persisted-scope` file. +- [`d74fc0a`](https://github.com/tauri-apps/plugins-workspace/commit/d74fc0a097996e90a37be8f57d50b7d1f6ca616f)([#555](https://github.com/tauri-apps/plugins-workspace/pull/555)) Update to alpha.11. + +### Dependencies + +- Upgraded to `fs@2.0.0-alpha.1` + +## \[2.0.0-alpha.0] + +- [`ebb2eb2`](https://github.com/tauri-apps/plugins-workspace/commit/ebb2eb2fe2ebfbb70530d16a983d396aa5829aa1)([#274](https://github.com/tauri-apps/plugins-workspace/pull/274)) Recursively unescape saved patterns before allowing/forbidding them. This effectively prevents `.persisted-scope` files from blowing up, which caused Out-Of-Memory issues, while automatically fixing existing broken files seamlessly. +- [`717ae67`](https://github.com/tauri-apps/plugins-workspace/commit/717ae670978feb4492fac1f295998b93f2b9347f)([#371](https://github.com/tauri-apps/plugins-workspace/pull/371)) First v2 alpha release! + +## \[0.1.3] + +- Split up fs and asset scopes. **This will reset the asset protocol scope once!** + - [ad30286](https://github.com/tauri-apps/plugins-workspace/commit/ad3028646c96ed213a2f483823ffdc3c17b5fc1e) fix(persisted-scope): separately save asset protocol patterns ([#459](https://github.com/tauri-apps/plugins-workspace/pull/459)) on 2023-07-10 + +## \[0.1.2] + +- Fix usage of directory patterns by removing glob asterisks at the end before allowing/forbidding them. This was causing them to be escaped, and so undesirable paths were allowed/forbidden while polluting the `.persisted_scope` file. + - [9174b80](https://github.com/tauri-apps/plugins-workspace/commit/9174b808dc37154999c119fcc3f31258a9c5a3fb) \[persisted scope] fix: handle recursive directory correctly ([#455](https://github.com/tauri-apps/plugins-workspace/pull/455)) on 2023-06-29 + +## \[0.1.1] + +- The MSRV was raised to 1.64! +- The plugin now recursively unescapes saved patterns before allowing/forbidding them. This effectively prevents `.persisted-scope` files from blowing up, which caused Out-Of-Memory issues, while automatically fixing existing broken files seamlessly. + - [ebb2eb2](https://github.com/tauri-apps/plugins-workspace/commit/ebb2eb2fe2ebfbb70530d16a983d396aa5829aa1) fix(persisted-scope): Prevent out-of-memory issues, fixes [#274](https://github.com/tauri-apps/plugins-workspace/pull/274) ([#328](https://github.com/tauri-apps/plugins-workspace/pull/328)) on 2023-04-26 diff --git a/plugins/persisted-scope/Cargo.toml b/plugins/persisted-scope/Cargo.toml new file mode 100644 index 0000000..507b5ca --- /dev/null +++ b/plugins/persisted-scope/Cargo.toml @@ -0,0 +1,30 @@ +[package] +name = "tauri-plugin-persisted-scope" +version = "2.3.8" +description = "Save filesystem and asset scopes and restore them when the app is reopened." +authors = { workspace = true } +license = { workspace = true } +edition = { workspace = true } +rust-version = { workspace = true } +repository = { workspace = true } +exclude = ["/banner.png"] + +[package.metadata.platforms.support] +windows = { level = "full", notes = "" } +linux = { level = "full", notes = "" } +macos = { level = "full", notes = "" } +android = { level = "full", notes = "" } +ios = { level = "full", notes = "" } + +[dependencies] +serde = { workspace = true } +serde_json = { workspace = true } +tauri = { workspace = true } +log = { workspace = true } +thiserror = { workspace = true } +aho-corasick = "1" +bincode = "1" +tauri-plugin-fs = { path = "../fs", version = "2.5.2" } + +[features] +protocol-asset = ["tauri/protocol-asset"] diff --git a/plugins/persisted-scope/LICENSE.spdx b/plugins/persisted-scope/LICENSE.spdx new file mode 100644 index 0000000..cdd0df5 --- /dev/null +++ b/plugins/persisted-scope/LICENSE.spdx @@ -0,0 +1,20 @@ +SPDXVersion: SPDX-2.1 +DataLicense: CC0-1.0 +PackageName: tauri +DataFormat: SPDXRef-1 +PackageSupplier: Organization: The Tauri Programme in the Commons Conservancy +PackageHomePage: https://tauri.app +PackageLicenseDeclared: Apache-2.0 +PackageLicenseDeclared: MIT +PackageCopyrightText: 2019-2022, The Tauri Programme in the Commons Conservancy +PackageSummary: Tauri is a rust project that enables developers to make secure +and small desktop applications using a web frontend. + +PackageComment: The package includes the following libraries; see +Relationship information. + +Created: 2019-05-20T09:00:00Z +PackageDownloadLocation: git://github.com/tauri-apps/tauri +PackageDownloadLocation: git+https://github.com/tauri-apps/tauri.git +PackageDownloadLocation: git+ssh://github.com/tauri-apps/tauri.git +Creator: Person: Daniel Thompson-Yvetot \ No newline at end of file diff --git a/plugins/persisted-scope/LICENSE_APACHE-2.0 b/plugins/persisted-scope/LICENSE_APACHE-2.0 new file mode 100644 index 0000000..4947287 --- /dev/null +++ b/plugins/persisted-scope/LICENSE_APACHE-2.0 @@ -0,0 +1,177 @@ + + 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 \ No newline at end of file diff --git a/plugins/persisted-scope/LICENSE_MIT b/plugins/persisted-scope/LICENSE_MIT new file mode 100644 index 0000000..4d75472 --- /dev/null +++ b/plugins/persisted-scope/LICENSE_MIT @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2017 - Present Tauri Apps 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. \ No newline at end of file diff --git a/plugins/persisted-scope/README.md b/plugins/persisted-scope/README.md new file mode 100644 index 0000000..c3ec88f --- /dev/null +++ b/plugins/persisted-scope/README.md @@ -0,0 +1,75 @@ +![plugin-persisted-scope](https://github.com/tauri-apps/plugins-workspace/raw/v2/plugins/persisted-scope/banner.png) + +Save filesystem and asset scopes and restore them when the app is reopened. + +| Platform | Supported | +| -------- | --------- | +| Linux | ✓ | +| Windows | ✓ | +| macOS | ✓ | +| Android | ✓ | +| iOS | ✓ | + +## Install + +_This plugin requires a Rust version of at least **1.77.2**_ + +There are three general methods of installation that we can recommend. + +1. Use crates.io and npm (easiest, and requires you to trust that our publishing pipeline worked) +2. Pull sources directly from Github using git tags / revision hashes (most secure) +3. Git submodule install this repo in your tauri project and then use file protocol to ingest the source (most secure, but inconvenient to use) + +Install the Core plugin by adding the following to your `Cargo.toml` file: + +`src-tauri/Cargo.toml` + +```toml +[dependencies] +tauri-plugin-persisted-scope = "2.0.0" +# alternatively with Git: +tauri-plugin-persisted-scope = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v2" } +``` + +## Usage + +First you need to register the core plugin with Tauri: + +`src-tauri/src/lib.rs` + +```rust +fn main() { + tauri::Builder::default() + .plugin(tauri_plugin_persisted_scope::init()) + .run(tauri::generate_context!()) + .expect("error while running tauri application"); +} +``` + +Afterwards the plugin will automatically save and restore filesystem and asset scopes. + +## Contributing + +PRs accepted. Please make sure to read the Contributing Guide before making a pull request. + +## Partners + + + + + + + +
+ + CrabNebula + +
+ +For the complete list of sponsors please visit our [website](https://tauri.app#sponsors) and [Open Collective](https://opencollective.com/tauri). + +## License + +Code: (c) 2015 - Present - The Tauri Programme within The Commons Conservancy. + +MIT or MIT/Apache 2.0 where applicable. diff --git a/plugins/persisted-scope/SECURITY.md b/plugins/persisted-scope/SECURITY.md new file mode 100644 index 0000000..4f09bba --- /dev/null +++ b/plugins/persisted-scope/SECURITY.md @@ -0,0 +1,23 @@ +# Security Policy + +**Do not report security vulnerabilities through public GitHub issues.** + +**Please use the [Private Vulnerability Disclosure](https://docs.github.com/en/code-security/security-advisories/guidance-on-reporting-and-writing-information-about-vulnerabilities/privately-reporting-a-security-vulnerability#privately-reporting-a-security-vulnerability) feature of GitHub.** + +Include as much of the following information: + +- Type of issue (e.g. improper input parsing, privilege escalation, etc.) +- The location of the affected source code (tag/branch/commit or direct URL) +- Any special configuration required to reproduce the issue +- The distribution affected or used to help us with reproduction of the issue +- Step-by-step instructions to reproduce the issue +- Ideally a reproduction repository +- Impact of the issue, including how an attacker might exploit the issue + +We prefer to receive reports in English. + +## Contact + +Please disclose a vulnerability or security relevant issue here: [https://github.com/tauri-apps/plugins-workspace/security/advisories/new](https://github.com/tauri-apps/plugins-workspace/security/advisories/new). + +Alternatively, you can also contact us by email via [security@tauri.app](mailto:security@tauri.app). diff --git a/plugins/persisted-scope/banner.png b/plugins/persisted-scope/banner.png new file mode 100644 index 0000000000000000000000000000000000000000..816f0c2978a2c33f75ab5d220c313f1a4608a34f GIT binary patch literal 47372 zcmYhi1ymHw_dY&LN~lOlh=39+A<_y8OS3e{(xHUXN(%@qAp%lT5+YqoBdmZ5D$>#^ zA-;gpARt};SwG+3IsZN9J)T$h%+B0<=icXe?wtr7EtRvBER+C%vuIUCJpj&h0f4AH zLk9omBku7h_y@&fRbzJmsOSj)Awb$|Cis^KcRdw(P~LNC5&jR!Lpe=30IFiCj_#8J zP?wEXl+%BLSeY^pH#dAUeay`ninKxBrBA9>E)x#@`SW6HXpNk6f(J@jPhNDZ=95rH zm_75m6r1aL_nJST)CF0kE>{i?wAiqiR`Ez)HkC_w5mWVyG=X=ltkwUeh7(m7r;pT9 zq#1pZ{0Q5`0gW%=}92u8Cuy?BfQldi6rB)wjFjj%BBT$D=%>R`0!5 z8y+2ZUG;4^se9z$dJ~`;@y-*! z3knJ*cUC8+es`y@ADoUJrmnR{vt~ShLku=6Wh(l7!pC>__kG5FK>Jv-psCcB%*ec@ ziHV!!qaPjsd(*!0fs0G|mf7*&62l59;5sn5w6fwfzrQ;1ZmQOC^b+5O`swoc__)yM zVfCrw@#6M&Tj1cR=V^EC5aTHAa)O~+q>s(NHw?n+-na3_IB~~I&a%x zFFCgET1P)}?;MB4$#&n(3Ayr^cXXsHB8eo<={yr| z1t3nSgonrBv}tdKiX4Pr{OrxacCxL;UgEVh$Co*izQf+Ak+t8OF5!F-xGg?9ms%Qc zJ3`JsZFah8y3C#zZ?onObm%l=4(WJsJk4CUz?|)vfw~C>w_};`TjLWz`R#Rxq)iSU z2dfc-IQD4bBaU-hGJr<5iSl-mEw=8Z0393Yoh0{-iGv68JsC}AfhU=x^DU>xzOs9= z?=`qG1!lS1pH|eB(Sjj4HW%k6o1w*#H^b0U$c5t2_5f4CYjVmMn=?^FDCy$U3P0== zuSc(ukpC4#Mq5HLax6xmKYw!Yb1a7 zEPNUw6E(frg+QRDhQ7ba$XE@ih=^}#QF56BaW~KQ)VS#<+4ypIR1VXEPlZcF?(Xgx z^9_%5%*nuJj@ML$77D`*R70k{66Vf)jh@5j?L~;5Iv>}^KREfujV!id=I$sZzZz4< zzBjw3wa5vSa{k7f(>0%%J_1B}REmhD(tM zb#$c2%(-{&Yqxgt{U74jVoS@)GA8QX=R1T-x?eX093KdoUx`;uHqNwxx97#VKfCbS z%mEHE9CCL0L+v(ggb0W$5J8KW-094_39{I6T>$k)Y&SPDAq)rqi43O?7XMTja*ibD zxzwg^-`bH?=eFlrvSF4*w<1uA1%-v{d`%_Lf!G>kT=8o1U*rHKS;-ZJN;+^uqpBfl zO>j~caQNdGAJEtJ?*Zst{5_%pU5(`IYK?I{wX_Hk<+}3-n-5!&wu_z4Q^19>KxlJN7JRGkYhcrJ)Y$q5j`b_drPn_5+MHCUGcYj zJMFxX{qN_?nHx>9!uAi|zW8?VSug(E>AQeG0Sl^$2pZ<3scl30;u%244dTv`-l$ay zyWFD4%-vd~nVFfn{*+sn4BX)3?%L}RJ{d!RxXZXsfNB^EqfmVw_)&y6o&A?TH1F{* z-{}Lt>mvR7$^FUd%jAob2ZN8KKzN+Xh^VOO)Vesjx~66hfDKlCw6dv<-9UQ8BYL-7%vTp0x4%xC~ zgZ{aa&svM4F}ikhs`MfwWBX%LY`eXiU+;bN-e&B4nOV1%H3gy zDQYAFG(^L20cZUr1h%G%lrE0ZwCH+N#)O=y=;~zsmfy1FDLMi3fTcv>t#wK)vb^l56yq!*w<-nEl;o;!~m`@y5V#%=8Vz;W{kF4#Qpgm?=&%zA!JlWGC#V4GZX#3e^WgFvzhqoG0?a5A?lb$u4a6eSfP6m9nU}%D*wplJb-VFN*@Vb2 z{_NlZ`%e1-``)W%Z8VI!J5#TZ^%m1E^>^w3)TNpMcj&o{h!v}xD4w_T^YhZH641w3 zp8bv;1DG>tgrY@{@M%hSK^za>12OPl@2cBJ{2R)Oqx?-ZGff9{a{c@pzW|j(%`(}K zy$IA?A~M%)(JN%6+n679?U)R7zvW`)7GpGIi{l~&;nA2^!U45J0QL09%VbrOV4OQG zB%bhAyI!q7ORAQ#U$K0=Ia^NlIl&d;NV=K$@$>IBsL7(iNK#JPAI0Ir!> zSggs69&1Eo0_va%)j#T772LTp){|a0RaeXZ zQUl@P!D0az=4f%IhX9pUq=(3RX2bk9_cmR3era#Nbha?$W4+JRgtCMG-su?c1%;I-r1Yn=ot^7ysV3&)4*Q1)+T?J-N8KzNz zNO|6GB$cCA(GUrtsg=i9QR~(Xh9gUQ|mDH zur&e7evY@e;?4lR+_j1t|9Qu6$D{Ee zLPX6-|AubKDg_4-aLYMN3w(5UCP3Jj9_XBBO-M;fO6pfxE2BD~m0I=UG(XRwl;hBNm@jt_=N31CW3y$awgCY`a zN2c5TVW6G%BT({U%rm^5Nb4l@1#m{a#II&s(e+ST79CbS53rN=M19%=hSgDlmLwq5 zP{Uy8W+*rd@_NsIxSLNDo+-;%AQYyHlvVQKbCIXx?ka5xGGBf)Cp4P*V;)9r!#On#5E44G@9xFXuZQ%yJ;;M)`|L2s^~0$H}pOujz&f znqzC?1VSh#cWu9XyDk|N0Z|b#3ojv)(~3gS_a9lQBD2dEpmRr4r>LUgVegyw@izLD zyW_Qv88{2B4gkhgCxn@&cxyE|0mla`B_Zf5j`IN%<%-O|)3JNHXDR7?=enLIRYjZ6 zd;9CSIt>|D0E1R$XN!3MPDDX(i1Aie8Ny z2b#JNim1#HCBwHnh>fihISQ%?VL~!a=(KbtEc@+1=%{O3h;H?i1b7Z;8U~r>Qd}T) zMu1@*>5_=~883CU_^LTY19AW=U#>-u$Ji4P`A6C#Pa#`3c7Q8Xvp9LuqHRP1@?uxb zur>7Ww1{~s9i5$5Cl5vgXOzX$b%Np_tNMe9`gl9|GD7g%4>gOGG5GTpqx#w?3v<@={iRAAX-JnP1ySsr4 z%3=d6R1-o7)t7vod(1c-vbmEJq`Wq0;zuhd^qho-1_!9AnVFg8eIx~0Bux!L7)N5D z|9tfo;CR%66CwE#L~td>)YMd3V#!P6DvZF>;8U0|Ir*wep7+~?^RWTYS{M|?=hfZQ zy|n}lrMxd1@oLl`35c}ocRi}8ow11P}e?{`}yQ$b>TFN7A;v#;RU?%Ly)X}k3pVhQW%~8RS*DZL^eB(W-kqJBXwqy=0y?UkX@2Il=H^15pN87E*mCu8k1r4 zEV{1CS_feDIz*O(VyBHy-7?{Bi#CIrDDRpE>$IK)%7u7+x^I*t4C>a`uV42XHzX4U zKtH#RErG9|fewQJzw81gGgF0J0>!WT%ngn!jjAJ&T`*^UKJphSj*udgX-MwkPp=y+=a zBaKX=6tBN6D?4p(*s%kX;A>V%7mttzwR;&E8B;p{odtcF!9piSVgG{S#~{F5mv(Y?BvTw2Usu*S!s&lhE{QZJbL(QrP$C5Io<_}i zl>u}@z^PK0_HAfrDD1R6X%)wlt7Z;ELPkvo^ChbPqX|q9fh}drHQ|w=fslTTKW!qq z3JV~MYORnDZ!rdp9-tisGYa&mDJ=p4ChADC)4KJ&%Az zy0cS_*LB!#a#ov<2sGySa5hoC8rP0LL2x_@2+sRmozhG+@YC}B*G-y%SHrdk$xCed z#sn@BpOx<5D)Kc`hYU<=MhH7dV>DZZ3`@|Y-0f!Xy`qP20-%W6wSCXKd{V|`DEpZK zJbvg|%?OzAzsay(f97k7v=DipwG%Itn-Beb(sY`;zS0RjD9HvCwGueTUyQ?`fcOX; z+h37Gi?oC~kj^A>DSY7sz_s5?13zBQL_-GZw65HDb#wu!lcOx@7mZl4N>aKdJRa{= zYU!E!21c;#;m2*E-CH31wPn~Oj{>sx4ESiz&=Bl+qF7xt1=GPwO?Ki(62KwTAj^Nh z)oHqMYIUrt83D{5M$N`PIwDbk!o|?e#dr6K2vkX)I1#z^(c)C4*M`}4^d?-E)>aJd zq^}#s$`AfNfN>inj(M4ynd!~X&wmbpuj>4^$AU`qmv*{(h*7NeF752Ib}r#nXF-jE z@K3F3>Nv9a$-Yo{ji;Bkr`OezjoI(ru0Ht%0q}*pBXyR-%TOqBVkH81{+96MqDe6E zx_!}+W&q^LSznI!kHY6|udjnX9yO-F^I?fx%_Ebb?{dJ*O$5{ioSH+!p1S+5{OO(v zT*3VFSx&*Hir_|Ngk|6}ZtyckZrG;W{C%rM4**=M*6iszHB~PgiBRdtb`eq0!Q(ge zoWPD<);R3e^?$389eZuaPKu{j}2ieRN zy2a`f6L2f^>SbRpG9SA6{2mpkdH%cc2rEqp)T3pCB7qR8GRWVwHkzDOp(^tCoBugT zu9nWg{IwoXffY*N1b>nOhHdm?UOR{YFehit!Tl`-*3`46F9E72kz+xK(#XWb%H<3& zX|3SP%|jEx`#Kg(adrYT8Ch&({M-8yP*juD4J6uwX3Qv41R$h@z|lJ3avbapZTu)F zF#|rHE+UkR*zXPEJeK*=e^S+Z7)B89-;*biaB8XHG!7F?fs`0=k2FL{QkSR+JZ`IW z5V2W=G6}CB$DcN(77h_GmzP3cv=d9P8E?ETR&2jR4cjoS1C+GAOcYic?&ZEb z$(kv<;pD^Vy(HVlMZh5sQ6(#nq8RfRO?uRXt|rtNnVj2fb^}PyC*GLVmM21WitY4Y zLFBgFJ&}Nj$!nvev$VWi&zMYAQMf;vbi+=^o1aJ4_&2*Wq*pvojwvo^g12sG{%+~|Ue;liKkuqYvHI7ta!63QURtXe!MSiPD@YkNA+0@d6WCDYL-ABXmgR|Jz6U^`MVb@WeKM5RP|P*m)^TC=#QO{)poRe6 z)5c`ZRvDR^E;GJdj1u!&9p4UbgcmFvZ!y5Ap*&2RLWAftP>a*e(WuhT=Xvq!)hlCT z3INt{bvmW(02Eh-%9^6`y)>zcQZ5o`2qEu`(pb=0U?+q-6r{2KE^8`JgHS~T&gFNZr}=n?<_HhYRYo5wQWJD zh8%bY412`e_SI;nOL#_hjJ1b!ih!J~Z;xefmPVKbe*xopqiSkYPWH>(F!rhiJ~>f{ z;)ew(M1fWA&xlsJlSu1U6abmOJCF87a4`g8ope9w_Law(Cl9+06g#d$PrrxEMuW|~ zNvzH#MPr7@j*gp(FC6o&+S1I7%asOzsAr+fplbOQQbC#&{MpCYifOl+F~^;2%51BB zp<1dPyV9V2Ed>fC;lVu4f}&=U>ZH#RS!cy=m4f7^^=dnYe4g-*8d&=ibiPEjnj|9? zzEES0Af*Gyu1v{X9Cci>l`S&|Z7hyz@WY>1nP!rp4m>a7+*(+S&}C2nq@c z|AA{HRecgAoK6m6;d}G}wy8-;4CFASWrwM*=2TN-TN&P24&YSy8P0)QgLw6TLtYjE zq;6O!RN-~RX&->E^6ql*b`oSAHQk)3Cg)q9y*St!yov=vR6X(c2}m|X?3NXqIqCCM zCPbWct&hXu5rc5`hYyqAePN7p(3i{&3YrcnII!BJb)`fUBSoR|SwoM}u|n5Y~F%0z9FIMG_4fcLzV2E{ql%-b+HwOT?` zpBP=9IHt#E!t zA3Csv<_SX(B$yky_d;49EL9-yD+h%?dqxVaq_5`uqE=jaxKzOPJirDV?TfWsID;p) z-=ND!dnwp>IeURVz`x}z#!Lln4dneD8E5-H7)_nIauI1Ell&lr<-Ct1744kk=9h?XS|3&rxn%3f0`U^K?La)kzHM5GSq71 zgwQo(GRUtZ?rEOU*4BQsw?6KomEZb(1SW*9twdujq0E79bU-=#nchiC`#3WrtnQt9 zFc_Y(6O@M{zL2q(0>oJcX zKni4l)$ zCtQrlJp^~IR}3C*JWq8(g|E^%iSsXJR_fRrEGSWks3mTMT)~y2-y;Ci^EKXpO>WFJ zY>nMfyM#tK8_ahO*%6p@fvTiP5Jz0=tVZ_Nu%Rg9t~fK<2DG)U+Cgn7v!(~1uu=Y> z?`otd-3q>RwfOt|Cg4-uQN6_W9f}%4B-o0T+fYX^!BC)Kf?TaVN$Lm+^sHp-EOnL; zG%7N~E(a-T!|=N*;n%d!hEF$mWe_##v`aPEPqem9FXATPh}6Mgsvs3V@i)g2EE48#*-hl)Qu5x%Jy;R<&pN`*Z< z=D0KZMEjTntmY(rA=sVqJoL~+zrzVR+!p;iXWVaYXU9fTS6cRkvJ>U>eQ! zwbW9I`_;4X=CC4#l@U}WT-n0YLqqh#+gQea&kBi8cyrqB{RQ~%3`0#~B;SMn7G*B* zVnn@TjGzZO&fqPH;?gbnd51d;bEtzL1n%RZ42LfFI8^qv<7ea^SZE?{Px$Stuj_EQ zvpRM9025ljg8-P#1lzhcun5zP;xD?czMOQTxk2?W7~gjMnon?P zQH*H2EBdpt+`T&HMD27hVB+-DogT;x$%QcWav&AYL1Z{Hv^QU21RY^RRNSZEC5Rz| z)$bw#z`4-%4ef)YllVry{uJj_?9;#FUg%lQ?%hf?CYVK=f>UX27w!@*Zt!=-{RvAU z1g0Bblk?C^?ks8!rDL@y$6s7*PhmZ(K;A1qd5QbBHzBgvH2uM65TTmTs)*y6C6|Wx zcm`4K3{c1u|L2Ei<9M2?qi`39s;Of6R z;!RrG(%(X-7d)gR#WILIrO|TkPt168A`tpu!jFZq6tURR@K4o<;9DkVS06T(2s7^pia1<8b?k>#z$@fvL&^DsVq}yZ%AZKz?yX4Gu5D_DBdCM@AFTxZSTj1087j zdt#K8y}iAU7OWRb;uF4)(}t@zIf&?wk$@reFfuaf4FX;j5mgH4I-XV-lX`{236zse z6Y|(^-q7A1c2A2a^hyk}+uq#qD-t1A%rO*a=59lx!#KEMu{fE$1j~!P9xXmop!rD3 zN(I)-2z$L2LL4O)ZI!Pgj?R4;7dDWFXVF*b5P`bQgiiez+_$(aYO!2Pac1)hwZ!|k zNkCD06Di#=5-Y|q|9$c96~b9k!g(Z*kIA82g5s-j(~A%}p;SBjm7y~G8zhgY+M%Wa z##cD~{|r;VfAo9z-e-%4K`n^o8dLGI4uxoIxvqIEEQe)vq;$JiAsGD-r*z?8K4>+V z3|NF+Wepc1)nKxpgnLT%pF^|QEhvOxANyibtCX13QaEoAh30O=#@ifmgdrm|hi#0+ z;6+e#_45c`L8To9@G#{3yT(zP3F(Y>2IK}o4?uVZ+4SJxAgh5c-;HKQHy91um6VJu z3MTooR5K+r&rW0+t-x^qm`aDe@7^qj`9rmM&N#@i)0#*V;%_4II;O)SWO(#Y(1caU zv6Y8@cSTrHl!kHvCcnH`kV^eqEb+d0B7H> zz7p%HQDWYCU6zX7KpJHW>$rAtm;D(IMzqQStPvr|$eX4+JfrlX;C;`gW%boq0}1L| z;?F|#%*-dh`}cz)jo?BrD_8I7ds+HR20HRY%c0%KI;%*vPUI2UI3B=Z;Pjbf?lWwB zQt{*k!%)B4Z<8y0j9|bMwy65txjRqBOxRwzBrZuTmt2ma zCJA3w>D=`c_USyEBP{@G<;dC8D1|Do#0V4Cf>R}hmrSky>kA&+(Ed6(lhSBp>m3qS1h|HD`J9If1_N`u>cxB^ ziG-ceYko2M`JKlSa7cXYj5WOnXF`DeAZ5$`{=QbGwza-`n(nD~k`9^{wRQmehQG>b zTT@C9An)sRF#7m5<2=zKpFuK0PYy<1wv*+P<~b@X(=}8MDxyDhV`k<`2AoF0ejD3S zZ^>iEWc2UikEszC=d6t4qb?^lkb^wPeX+Og1eB|(^^^!pz&z`uM+>ZSygDD1&@(}D z#LW&*w<(UaS3Z_IJVmF;Md}OQoyRkh?wRVJLBonYw0L=bdiqQ`@!}>#0PNbQ^w*%$ z_z5NPAF|;yLHcqcFeCv18hW^pT|@v{bVPgTw6IFxSRoIE7Ez1qZ=SX=P>qNBnU#YB z?(lUa7BwC@DjG)%>+E0Kgco5QN}#GAcRw)wCKTtPVqao75H&XPW#?2h=mpc_Q%rDF z59VLXXYfw%`SQCRmsMF!Z*bbe9JJu*>)UW>7W7aP5;Tc>?deeB^y-ajYEuh~gUunUrkOr0slhZ_hAf60NrY)@g6|_SE!30%B2b6Ovg~ zLY?#-Aj!#`Dg$ERD=I<+!W?hUFBdOfOhrL(iXy}UhjoNVPCyR8D3jxkHImU&*ReC_ef<|B?lscTy=q;$%lX&CF(o21N}GR}^gm zCuHEMQ9^1rE9ZzyerA)9-VumU4r1Bnrx@el%J75XI%;7m_}xd9(4#V33fza?XFlS* zZc7hscE)S4ett4P`Z$%ul}n)n)73aPj9PvnO#D+HpYPBmpxN%JBYH$FZlkR+ZEjdW zR-u+z-`;h^KP7T0gB2O4K@k%k8drN-m9y&ML@a+g+UFcyp?sg=N(c}Yk(G_A~ zCptknxDarLfb_pXy0bWa1x;{wH3i$-%F3T03r%2PAnP37Nb$Vp0iS*A`GqkFO_O_8 z5u&AG%HK(t)oZM31wmGKX?&(m{dv_)cI~{(7y1O?qCh7#UNL1y`@Dw_%R``GuwQJ4 znHw0;o3H2YFnX+F{cxfHjiyPX$A4uu(8Gr~SLNB@0w_eR=EHs)-jsxB>7l7P3vL^+ zxb=>e8zwaZf_ZMxZHzKOi;(0Z#EK1bzHM%b{4$2;)!|nU)wLX=edt7*&#gJOo5q1( z;uMhSc+tweV|*wVbcGg5NDF08PPr=2 z2U{V2EqL%gdTtqbUKRyY%!Q`Ebtmbh!D0pYIc5xw)AfoV{$sphGb0AMSf)mrfB9x0CKNgAK`XoQq9a1u(syp zP=YQNocGo*yDbKW{?Q3A_o?F2?4!}VlXB-@v%C&W>TmiD{jyl3e(V?BG*vX)0Hq(PCXu&(mzOPpDM2jLcX z9<8S3)pf^aB3*_xqqrT%LNH^$#=O*8+HSNs$X;@Ut&42jPJm7ZB|U8Dk=#7!R)tgw zrT+b7QJHzFG?7T>okfHQ6%|!!9?a?}FM*8~1AWsg&zl(H7BT*<77EtheuC=1<=J1do^@=lzwpy_Z1E=a0KTmciHmx$554rtyf4cMN)!9kj}k zrL@uXboNoSiUR1oJ+r0>T8D!>>`KDfEsV=!hKHxer!EvCxm;i9jfdF|)vRE4?d}gY z&pi}oKw|&yPjr}MvZ^vC!TgOX`?q({xj|>=bjVXB`246W+!vFx@x3>{2lvZJFY5Gz z^Moz}(1_7HFLN<3%oyU7@~&7PUp#oQcsCDe_DC=<6d0Q<_T z!$nNbUjN%7U#=oECQ;W|nV{hO`^3E?=uVj>IsbfS*OS>P($4VDXAl*M{!1lYBp*bv35#zA1u<-id?}N=&MnP2L(Xt;wD7^(S2ml(2kajFxNU}2fUlApIbF+Zd zV?3-w$DXCVjbN$K-K8bIj>qli{;liuk=FL#u*knfu}AyvAvlRL=M*p(gwt&&NG0H& zfV9_2oulkbOG}GZ*scHlI`+f0xHXuMA&7dap{ePa33u#P0|ncWHuk_tpZ1;%>8T$d z{id4F%dP+pG0*tI!ov5{5REd}8r_0CkMOApVH{-crqc5vYJX1qAMBt^!W6~zd#hL0 zTXw%gdouH6G1KeK#cTI}OHc2|`29|ODAkL|au_f4c<`X$@J_`43x)ScxRs2$fa3<@ zxLB(k>aElE{sK5kB2Lahp%(A6|Lc_>PY3-q-QVX=ex8>?i`LEl@LTr;Jh^gzCMV6_ zeZb+(^!}Aq*b^swuwH#u4Fu=D;SZOQm0bsO)?Qxtbeck&o3t}=x!vbbv?qJ9r(?(e zv7g*8R%EXC-NqfhI^DGNI$;C)*(u1Y>gwwMzW~^mQ%`)EE(uj}A1D|Qnlc2xeEZ>p1}~vgiXtgbSQ=uOBoT1XU=a*b}hFoQ#- zG`BF}``yz=nvxM2(%ubC2PI8QPn|{a_la8PUzyQ&UjIJ}uhzf`khL5sh^r37uuu0U zN?oI`KGaV}Trq5sP#bSpQ+M?QL=Gvx%BRX~4VHU%N zV{|dCMYwN5f&1#C58wjI?;JC zZ@K;PXvq-n?bpeh^85ahhfxTyk*iq|6DUx&-aOg5$Frz@fU&JnI-HrwfzB&4eb)fx zE$=rK<>g*hNvLxCP0)WZm!NHi8LB_tn~nWnJY`XzjXzdoUR7`S@L`39-QVE+oxgE@ ze?F5-(=qz(uC3nQ<(2O23k|=VM%{mBVNhGv6HWvr{eTP`<#Tf9$J*+_68Vd;s>6oB zz|oMOW^ey82h6fgKR$o^&nF{)N&h!#H9NAS{D$RykBG5kV;>uE633gry2{vITwITp zXcO+442#K($z6Lna?ycR`i)eKugm;zq(3^aeBq(&Vd^3K>1<#fG><3a(6#$vB-|Xb z0hm;f%@3IO+dC7FyvyhLG!B0J z4e*%U4oFMG;+FO#zVqZQbns@bOwO0osbIRBdfQ%`6}s=VT(n%#I5V%6*}-4&VWuZj zcAaPAXsh>qu1c)(a8u3jmur6oOLE-3VF}8p;9TF3ig8FEjvcV(8`f_U{O#&6_1E~r&bZ^%%s|YJv$f%{xXG`vM#_*i z^8li&1NrW&KR<_LJYO@d$1i#oEuSD8c06tiS$r7VPrJ(2r11Ql=Ry1d%jT04cqgI{ z*H4d|-k<)>Jh`jykEYvtQaPP6UcPUn|6M57Tw^wjsR$Q^>k7wxdwZ774KhKRD%pcv zS+~H0!oVe*e6@y5hX{+J!=`|e1kh=-Bt9-614}DSrzeNyYD@aE2Mf8^ZQ1?*>gLl$ zX|mCovo~nra<;rqPmZItHH?G5!DySZ1M;I~WN}p*BN+c5k-bMf$M^ZgfvS`R6KH{HEH`Zkgr}wHF85~=fH*TjQVeT$^Qico*&V0rl zv=0*>56=k?)%yMSsc0iuE11IjPA#=DQYFLZrdCYsEi%U;7%}fsz6k`;#<0)~f zOGUmj7Hxa&FRzwsagLL+83rRliW{CA{F=`y1|QZo8b;qm^D(`??K2Rqu!=dyPiBoi zK&rQyjXu?AvpAO6_DMT)skq5~ZSt0(_%=p=s`R=huk5x}iS0HFsq^y#G5VSjG-|%r zuMU5WMQaZ9o)yPem^x9>{z)>L@Zg@w_iD5|I)NjF;*FrFsJOlZ-SWgm4TBNleL47~ zvRE-s2byB|j!Sqy;cdsaZyNLU_(+YAq@aaUQjhHf_`NrFAJYhq);Sh)5$+p z`_T1_Cwqbp|K+qjC-J(|7NLNC=AUgWPbxpZE&s2fd>Zd?}?M?RjZJMyFE=)T_k?Qt$Azgrp8Roq_TvXaHS%!euB zzuoVy9641By&3#C%Gf`Ar}4hicMG*-VW*~02?DI6IUDE}p5=F(@-_N-IjJ*U0avQN z%*8+vMKs$P1v+oD5>*U!LV?{YquyJjl7HV=ReZYNrj4%Rk~&w|dv|!uirQpfz+QPh z3C|Rs5S+tuUaze97Rz93TKLbcLn+}m{K-Svep-AC)q?gsBVYAQ^%)N9uYIG##0rbc zPkius3&jZ2T|Z1R!VPM%fJ16zL;JqC%5~T={{=DPrL_ErbKLEAdSb&#p$!ih^k)yw zHWQgaszM<%PBFxp+?XHmE;zHec9zlw*`Cxa!OOXK{4Q}^tkn#4kf%8ML!k6;2_Oi)FEb8&kPniq7Zacq0w(MT!Zyh?*5_^8K(SO2!qvhhc zf7l0WXB>eR`PDpt!gPq(C2KyNqYbZKezp1E@Zha4ly~P)t8dE2NtXI-rUz)X^gari z$e)pmiFR9P`tB*0MH>F9{BAsB^Iho_J<>!AkC?ACw?_}#|NCKuzN{H*I28CTdmyog z%G~B8BE zvv99w{k!8imbFxMns`T@L(*?*duSWPd%IY3(LVU1!>!I{*gC}YDifItrP=>?W@eJf zlD9rv4!K#zt#jF@trA?FDN^NkKJKUseVlVM9Nqse zvD7SF(0cR8CY1mHvjicaN!o*jb4q&s!z|8YdkJHhe_m$q6Pz8YN5f|jgLjVpG`qkz z4(IHGrH(QK4vG)I_XhmF(O~}G%X#6*qA6f%s?N1JTgEvhaapCqrQu=Y!BWY&gu_$S3&e_i|jRul7Ldw1!w zfu~zZPZQoi`fSpTNqj(FsFdtPdFRI2%{E32I|4-J`!ls3AbFseN&{ioFN2e*@DQ>X zr#7*)|J}C5wcSoy!ZW?1(a#~|x4fq^LM3-}%EMx;Ck!?@1D!;}8Ze$c#j(DB7j#D@ zoa-w1pA1VZy~Ah6pNo?+FN_ja$yH-?&UMdQlK2=M!Pbb2Eye{c^ec1r^y2SMzg|tz z8G&yH8+y^naud*S8mb*vFppj(QXa43aKvItL1%(je>wf{-gz}a();C0u&KGZXVFFC zVd-(a6zm%&$6d`Bd_9`W`O;qm^V*$~rM4jJ^uo93!rqIw0z&8}( z_qO$KL5X)ttD-0;N~)>o6;FK1r=!Sn%NP~UqLGkGFYnz^#qI||QxKU!@< z?p-%5vG04)iE^yYurK`-eV7uH0PybfJ!@a<$Ra$o;vMx)wnr_!AxVC`plK=u$0$ZC zEDL8R;u^?{{@sp0&F7^*-@PMbG2P&`yYhYZ^3y+`TlsDxX#(cjFFfz~{&k?FqHzgp z5wP<%Gny@`I%1+>eaj)UhDFPto|*;o$7x8O;eFtAxIN_&uy}KV z50?A0m{J{9QEH0wA6?DJ27Ll&zEV8z&~!WRNL8Ee8SU3qv*-(HMY)9e7Hz_ttDZt6 zdeie$-qWj>)Y%5D-aG8J75tXy{K0!@pA(#sk*P+F+~F42{=0kfW7_@nC;JicuJ`HM zwCCv`_7erei)`xA_Es75!@~TRE2`62?z%rhYPTWr!k%nLf+>V?04$y$4)o>cCGbi0 zEd4E^BY+=L_S+LS8(bj+uUFyYfM%c(qRHKr72rwM=OM^nSt@tLk#=vOci|z!Sao%i zr`Ev2!SF4$FQ(#RRe3)bD|;`m=AkqB)?P8%==kZXye|Oq)Ey$c+HazE#`XLzTi-A^ z?woCleUDz0$+jUat1G*@=lb(gP}RZL18>gncTQ?YEm)sS*SO0Ch6IenSh9_l(tdYW zPp)cIQ+3FMZpUYoOTIeX5q+5@wTI2+R8!Vt@v|pcs*t4{tvLJJF#X@kT!1SKM>O;0&2la2MdFw`oEy5XHQg51j z>`zbZ+*v(i3`;2|>YSa;JM?USzo;2=adz(Z-}(MG28HZ z|6$b24C{nycm0Upo|C+-Y}5Lh&bhx!t>_&yxds z=~C|@)LQy;T}Vh;EH|cui%Kme zHZN5F1aL{V*IDK=z48=y4Ex5z?K z)qkJE*NZVhyP`aP9Lc7?`?v-Rw;A4eN=H8^vV0KaCSgrRL=| zk+BJXva{rh96zmfVLrqVu|e7|x4tgLc+0%PAyB@5-b4vgBsFd&sDq@lsrsaSW=S-R z$GQ{^~j@ z+ua1!2n&dH9=p7S&9EDu2MT1)g5hBm-{6Zb!{GLr87toVWZh>V;lgPoZY+u~pSd(< zv04;En{l(C=mpP~LXDh^a|;O*RZeKBgxFH#_c&)422)_wt9L3AUMyFi1daTSGes^ZB`M3iC!;n>dpS^EQcxu(qoweB$KyMJQ3ygucF! zA9*R#s#t8!g>O7HRgw^-bkl^ON3+bg(gtE*?Qc*tsjPI?>zpZxl(^vIde=Vay0WOZk{ z)H1^KTJY4(M?cAB&!nyJz=tQUMaezVJRQ1)al1Y|JAE(WiR3j0+6LZHWBgHVf;QE< z*X0c!oM8aNl$$y*3E;9NjDmj0Sqsoc!!j#6^pJUi(Up_`Zs=rduf92=$ZjZ9U|Uil z#y#xS{~ObLFr>!?H;S=Xl%)sTim_DnctwOPN*U!@pzB8U&TEZ#3l|Q$( zJa)OmuCkn*Uw0sUaeAnvd`OuRiOy%VqJbgU)U&`T!DWt2*MrJ{kLj}ub zw;umQaXU90Xw4eDqN-(5?Rf9WZL?&A>!o$jjfm0xzI=V|X+QE<7?UJyi|gDIJE0QS z9vb1B;f%Fkn`KR$H%x+qATa_AO01G|9a$x--E%rt;-`L>fhc*4O}fG6Q`~-eSj`RJ{K3*beX>e8{@_qx59B zBY~F)3~+Tjhoocc!)=bk`KJW#;NIXIBqj->qSiKu5tXMh4!gX-o$Mtz$7qwCz}IKlUe%SL zEz>bine{%}Hr%1!xvaBKyIsOO;vy$rJrdm>*@N@CTNpQdLnW1;!ZSK%py^LZ=VXJY zV;)XKo?&B2*gK~0=w>k9((1F|IAMScRd!~`Ks{q(=9AYDhT{zX(9g!=pOIv%)RTj` zTa{<&>g+D>`pnrRuJZq$pEpNx-In0BAbU?*mt_2=XRa)62+ zZ#36S5-S?sQCBwp2^ITu8N?X~oiKUu_mF4?rCb&s6rH!aFso7RR2I&>GNXsfu`+04>!Rs5p-cakN( zl5NJM)#ig2>-xINRDa}88eKzgYgOf}Dyv5F?sbUFTI`~<2>4+r(k6B3TThRdRNVtt z*O%21FDY&nJuIP%QW&Pd#-)rYOj#|OhMoJ1+$-RCjbD@dDDUE5yk$rs+8{d^K(xxR zbXop=^~-p61+s0=57U9C{=1N9KJ-~3z+!nj2Nwm5;dH>EvmrNkqka0Ki2R$Ywg=at z08^5Oiwa#KAB;f^57a(NL@Ed%?y>R!RJVK*v$JUjs_bM8x^ma=tV3-Za)wp}tCFe_ zrI7y;GjmM9=BP8AT@l0|Ly`L~$`t<}QSTi{<@^7Sqa-V`B3Wf?AQZCqo(Gv(nTPC< zV~^~tjAXB4MYcF9TiJUS>Ts;glXD!t*XjNGeSY`f_u;;;>$hj8uqW#Al+FqlE|XNVMy=nG5$gqhJbkb%I1 zH2p=z%=-Bs`lSa7JVP;w?Twr2`HzFoUm^WcDg2vDkz#j$wz?Ih*bkoFBg?pPMjLEc}~Z)zr&c*RFWwYgZ#M3IZ~qHY!Ng_!Z!+g_54@F_+-Q`LgAZ z7nl7<<1iTo{H^h{-{21`zVOO@PnkS=4L@if$l{;{wv5h% z>Kjno>oWJcM1i&oP~BdEqvDx7{N|sDyz_?15%|Y|W7O2J>_#}1^u`mNB{tj`TYAmA z4}N==XpnKW2V-%|w|_ zI}VF}D3qjLo~_)Uazb|Ws5&tPSET>v{Ljy?)f!==oGZ-kpRPB!pC0C2EG85U6vr0Y z9K`c7mK`dY$>|1_ewwig?lAaOQlZFeCFHMslr=mu;ul`c`SlCZI6pRWegN20_z)Sk zghPF|ynz0-H^n|ScHp6Wy^EyC8^I4i961ro`k;G?8PwzJP{Eh`te6~F!*8hd#X zO3%C}x>?;eS%ytk^7M2soIDG4Q@>o^$}SiXZ~P8wPcOPS@s1ED9D2fSj~aghAQGjy z+ZdT5BhND!W0aeuphnNp9G#s;{5}0IN85#IIt|z83z=Q5&&8QE1+jO925Y5@_TRvD zHodq;V;MV+#x*#HUdlHjOxINwI^XR2JG0r6&t}`yS0Y%f64A~K?vpCEB;*+aYNdnE z#1{m$lp&fvRupPGBmonzq8L(t#lwYVgF!AqIpM1~-@Dnqf~yHc=K7g0d^$TA%B|)6 zwyqY=3Aul@Blf%cGE)809G-rKzB!u08_qxb?9(P>Jljw&|E=00hHpTQwx>$A@A0Is zqduQ^4}Bx$eLlx)-Z7mn*$ntoLDYg z{djb-Fu1c!zghwuE@@P` z7}!U6M4D8W!0AAdCgRfI2ka;Att~P^!HP#F-)Q@@y$vHjdzhG&&5RO-OI!@AVKXEg z(oOXGiv6bUewKgzLS27iMXo1HQePwD;8aa7BumRi;t#iP0PdGQ3vLLt=8nTZYG=*#?{Gzu;I4d~fQnq2;zN;Z*w>SXaf@cQTC@@^5qHayRcLtxsH z#BvAf&ys0`&eN%vut|9L@2nv5)MChV_pkcE18zTsb~Ph#JFYa(718gLq%q~k>CZjM zYGR=yYU326@6_ODMp~~=>)g4@JwkWY)PKA)9EP^_`nirw-tTErlsoumD^c*9L40Jj zC$nwoY|CoEGggRT*74&d>h#IMG~-RwRAnxSuF)_c3ZTB8(9lED3LcVoIqJ8d6e+aC z`lR_-vDk;%`P#oadgJL|l$*sw$Td!>zaN68n92>3-?*=6u_+)zYz&d&1s)*8CiJ{I z&?%7+XWrI_>9DF@CCK+8knM{FitFXnT%Ivc2@zyP*fjDF3O3K)kFWS?oL&H@8^wz{ z?fxZS@pepUCBv|qtbJ5B!_b}aEaD#@$LApvsIQsfwtSH1&niC-^jL0Q^(VF9QiA%r zE!;SLO%FZBQ%>gLi%oF``DdWkyEqiWFfQiBSK+x`NQNIaN8h-5k>0=y9NKBZOw}u{ zC-SA^TYk7Zheg|YGjw4ZX9>t;iwXGHImPoHwnn=Ofl?5V&UpKlg@DdUxEb~19e2*c zts})J4fSvJm&@bkC#XqdluV~8x?>lO{P^ou7hAoms}&2I)bm^t2Xx`AiZAG8bIMta zzi7Gz=|O%JXt}>#J+s+}oqA`sKep)aM!LkPRR^a?F9})3FBjSg)*jia$+?lni1lyZ zzC}UU0M+jr0fw|rUG>+uMlRY>I*7CG0g-OBj`V49h9Xl@SI)c}@v0|kg!ReG4+i$7 z-;exJUQ+%Cc4CA^G|xjYFLHa7g7moLe>dpHiFL44Nu&W;6ikos0N(7Oyoh35W7me& ze;XW(z4X7~pYIGi`5DdT+r^F?$s1|{n!ZSf5r#C?)lCDu=3@nG zsTeNd(ejJ_x4SdyDf{8;*;XKq@Npz*gp5dIs}$P#dVlbI0}Q1jP^qNO@>}-rYn%c_ zkN1nq{pD|I>^&*->4%D(sq{{0uINS?Ny0FjWi{&Aic`xcvbous=CauGFg*%nPM(O@ zdjFesz#G=!hNmuBFSz|mrdC1_$mUekKAZaLD%q2^;0+y6^mJJU9t!Ov^qey%0*)jL ztZDZWe)4?d8z#t?0o!Jbc}Kuh5a7JjRKNHC%|vokMAIT(E0UL!vn4$v>+9q+Kq&i^ zqsdmk>+mxPo3|{BjheaY^*&c`bzwsrb=N-anb7$|>>dJ<^Eh=?@zKLzgxP+>`{Zu{7<=!&!xWr?L>L_m9hTl9TkgXxVzDt zqHF5r&t#7GJFvU$*M6Cd%Ws>lm0whLGf63ul94&GJbuCc+Kw2)p+~JfBPKT9>VV8k zOqJGwT9vFD2C=mx^Ma-0s!OOzxDwv{Ii*jbn3$;5nwg=K9x*7>_95ERDjIzG7|xrD zIlKLu?U&>7S@&z&Yinv9Z4Php1k2#U-Ao~$@e6x@zmKi`trcvp@y!(_8Qi80i>{r3 zuO};`Q%^JkmvW^(O+|@CM7-K1p8m_JuI|BTfkL`bxb-+I2j9Tf`atSs%f4cQqC}R| z!mpbqPDD@XrSvJjnF?y}S>5%2j_1StFpr0HC9+DX*MFnFx#noI%#GanIEjRhJnEIs zUQT6gSjbFEINnd!CB1YxdrQxCVk)sxEBHq!$s)*P7=CrkV`#M=6kC5|>P4ZPtEV_s z1Duul;|Og^ZQm}m^wjbzydz`;Ni{o>dfMr9bLX%5ej0njr&k20)xLKW)jH$ph)a9v zzqFnzrj>*Y7Sra&$_r?dWm~np_)I8dP#-otHEHDT^y^+~^5&R->EXRH z6micw(^6{nf#zK`h5#K-NW3ymLaM4T`R{AJIz#@K2Ln8(=Cnm9C}%C z-HoA4)4fRe$M08{9!+h{Jk<&lFAOMYWBdV7(%rMu!)rfzm-BmC3yWLv!sTazU@b@C zz_Gb$nC7%5{XI$Myak&ob4p8NKRT7c$z-qyb8>QeT_01Per2-lw{q^23xH{7UVWW5 zucMX8OG*}#Fs!2$|I28V(Zj&gX?Nk3Njk@t0(Rgb{PFbUi8Kbs3DGRgWV9d9)p8ps z@#1g3EW&<;&$TCN;HZ*r#xCa^;7jT6sq&XAW+K7UYcZrff#c!@8UGC9bdU^Oh~cAe}U-Sy;psN(4FAogs#(kpFsT#C!rGlnaH~#9ejH5OFN8Wfgqn0x+vDG^M1_8p^c6AvmCt~YVnFZ z!T`ZGRi{o&*1*rF-e-b-C55sXFyWLZbb6+F^MQ=NGTQmr*t@pop@%Y}*>!4O7fD0z z0r%PKK#QgD(mJ1qifwBj%iWXMPhAUzRl(zd6iOEmJ-Z?%t+Bg)2r~?_*0N*Xe|=R& z2J=w5@L1J6YF2>Wt00zL@9V0*i)}?`dhmlyy8<>wH)I%Wa-H zu)MTj4(I?%qT%3=B)iKnCu{4^J?xW6uMsJ9OA}cuk#g#DEv;_dI!;)tTVJs+fBNBr z-ayN8?5FoG@=4eyv94RydJ$Jo7Swn2Rx}d5bNLJO&F)XW5h?mX3sratOFoXgVY%_D ziDT+%!Bxj_-E`4jr(lE1)nr@YWa&o`a!5`M*846rpxmnnSZ`YR0SKg`K_Um!sgih$ z&&SgF7QH;}+OK59!%2_q?qbrf@fFwA>tdxsJi#tR;O#`*!aZ_byLRVo+Gg&@HWE|; z_szbn2y$_cW`_~hx3or2j2`suWTp`ut;RiQn|+$KKFh238)Tc;%HxRtsQ6%xoQy@e zs6U~X0~g&p?bIG-zoTM`aPb=VHn`gbC-W-65J0NBNdBbj&P+a*GbVPKN`j;~ZHtJ- z6r`x|0>q>148+`F-+j`wj7X=Vo%sn3g1*9=g$%=*Xx@3dU84=~faI#(;6UE8*=pZv z0kwem2EiFX5d}$@Kfa)&n>yQUMJB3SE2+!o9K7dY{4oL^F*X^YU%oPg%0m4DmP*_# zqED*^uqMqf4a^GIxsW1)tQImH@S%-kL#PHl-)HfAjdYoz6x@Wi!p4_PYJ>a7e_RoN-iG)CTrSH+kn0Ah;PZr2o ziXfvL7+p??h)U4tt!BH+o^vX#mj3;f#C#UveYC;Y!xAOJQ6c0FT32-5`8k7n#ro`)vV0Hl@}I$0lYsYuhS1t@`*qFC?&Lfe}9oSnzw zBWfb_9oTpc!?M$)vwqA)?}2RI;NSutg@;yuJ|1j-+Q_A4ac zW(78I!DT%Od1RZ@+;r({6S&hKh?GIZxr(Z z@FHArr7*jkisja*ol1OQ$5v*PX=uqB2G6yXh&Z>Kkxig*ylyBh3K zfycb^i3?lV3s+AAy@PMrpYyKoPp-m82WB{E&({Gd=UUe@m?##omXuli2yB2_l7Jwz zxB-_!Dz0n%f_+kUX^=lU0H;Q>!3O(VKwgqaN=v-rPnND)j5@)! zR{fbWEcPEJ+v52vRzt5m_A%-s{HC0RJXcDYsnFG!*9@r<6v8tdO1j_Vp{JGDvfcq7 z(y7v<@wcS@mQ#8b$}<-Ir>S~3Oe5cslXmx~$BD}t{e!X~cknAGSr8&r)hb5`&nFhd z2AcJgF+b=y(l1$1J}O%4@|9watm@B~ea2FW^x2Id>!$rg1Zt6wyorSvIA3wuh2=>P zNz8jUA`58Llv|M%II20cUgLXRFG(NHM+w=Q#kNcbrc?g3@anOx-X~Nf4WaNkvu(LJ z@3}BiMk>6d;_?Tdt!YX*63P$*zHmm*lH@Dn5bmbgtw;(^NZkx}!5y(9Kkm`uy{Vu4 znoR2~^1ZGNO|hkG-(_|#j>;j!Ixg-0Vls^w%DEBJT_y+`MTtk_&m2;PbfC!Qxgv{xIDN<6Uw5R5RCNO2F zOK-Zs0&4B`CtY_9cNcl>h%Ji5q!b}^jhiTv2+ae=V;GL@{A=(F4v189fwbX2PJ zlOXL)xgoF3=~$=|?nC+E7%P%x{5(u~PmPvaC5e@?^LGC`slaK7TxjPg)_v>m#;wVH ze~?`EW^*=H6Urlu4W{;c99<<#lcz3+EI#@BBmDkzh@pI3I^->>hx@pc0eZ+IDVM7| zs?*&Rp*Hn-vW&SIrIs61F&W+{FBp^uf zg=rJ2Yl%#Y5AZ^aq)3@I1jhHryHlhUh((S9kEar$`wqwA8(Rem(aw4O_Kmt;bT6hF zjnO{D#r#JZq>xe04jMZzI{r;VXnu+!PeltvML*Kyhia9TH5zS+qdzXHLKW;D1|<0q z$8SrOYT^LnDsYRc@*mF>^S#7I^N z-|4;0^aiIO)~&rsmiRmowd%wkNUw~{@rT2SJ@<{5C=)RcZL%GcPE@ALdqaL(X9QgH zE{=yCf{%U&*7hUO>sN0^CaR&PRPHjGIHZJfb#}*3 zEnk&GcI;ZmQ*kRmvd&TFpKP`*t6S3k(Pd?+S<#PLS#SjGEV z?>qKRyv>xDrYC?nhc9*g#gp4d$&5-$D2PcK5_EDRGaV|NM0)D+qUbT)Vwj{)^=<{P~W-ZSr8`xJR@YXxS zBa)A=#mH@&31VNz0`G5bJkgtALyhSOy89xUzjdJNVp#V63{+mf7$rXJ~z!IBRYW)T+PGc0in% zqIBmizE6YyIgqQg{8L<|{?o-B*_kyoyx~re*V3oEA#jJBFKVjIOVT*5!v|t{bL>d6M+8OIlFMRKcgO#`>qea?-*tkmntbi zrDUf1@=dP`zH53W-0ZgGNq~Pd!2;kzEJ4r@VQzM;xYJ*hKLmJOsCm^?wW`rXD*U#m z)c^VjFPX}fLiLqknFYcQHOkp5(smw(&hm9>3!8~cXhU_}s)au*#D&hA`e-8cr|7+o zv!Od)yg!o^#h!h*XV&DZn$e1vy9tVeM2PGxE_nZeIWwQ$20$|WfZ#7QAcuSTJ>ata z2(INONW3LJ-bVTzpmGsAZ*TYdY7*#Q_Z%t$${p zJ{LvWwGM(9{y$j61vWP#3$IHP8(jh0N#6NU*)`d7x_IXyGAhc^^ie$$t;@VZW%mY1 zPtdn5T{(!@>78rkA#e2xzmY%q^rP1vSK6fYS0IZM9T*c92d!$E?LA~tjiiy@KqMha zWd6!+!8@Wf|2R(AzH+O?_KNaPJ41t z#D*(BBnY`RXV(=;euLW-%JPx>%7*&40bEP$`LG242{7!n_y6-l4xQls3fPr>o3zw2 zB`#%y_7u;sp*!tK3*A5MD?7N9EpAG$WZb)S2CLxQq5H zdbR`j*<;z*sPkb^kaR!;QX5jKJ^5Ns!w(HP0Isw}_;c&Td!ucz4N-HGyl%+BR}2k@b%{cD=6@j}-BJ;5_h zhQDKq0fXCnG4?clMvcD&EA09*29mrtpJ?K4k|ZNWr|jPcpZh=JYeyjI)_+I`N1vZB zrl`|>x~+F5CKdC$Esyt8|eyS&B2>_CxB5cAqnFRYg2W1kMv= z5jQH8%SYaPJ!M*Wax0lkCQEzaK{asoCz$%TbHsnU(vmHrTIU>d<#)DY{5_^FZX5mg zm-Pe%du`*!>Ocxc7g2Nn5J(i;_<8&H0W`(UvOenZTN?3(xYmFQv#~eoYkIjhsumP&x7DO2-e()?%krm_>IIIFT}gTNhNQ`sC^-y_hh@Mfmoa@~Wt(wn zCS4`=6Xw+-a_{-TFvit@{Mo78ODH)RJL~ze+KX0T54$CuYm=f9{~FRX%3so{lY6Kf z-eiU986HVX(Z0*^XsX9ocH*B%+kN?y_=f}nL00_1Z;=;#-s%!3c5PHXsPo)Tzlt!- zkmqLgr{F5JhYb0eqJJK#(nA$vTxxcGCAxhzHIoajy>r;1IgoC(?Yo9IwBNHJ znRaPD$h}4^)p`Bt%mu8!n1BL)sNGM0G7l;xESwN-Y&@m;k zEU_}tor0FQEu&I;Mnp}@2|`cl_`ssg@M-cCcMhr)U2ALJvtkXQ7c?mkOye#ycpzYv zQG>8LTfM=|oiT5-h{d51fPjOCYWS!8hbt2FbIgt%ELGZnp>yEc-ll`7 z7h4HJTZDbM9#^N8y^7M_$Q+V3hZJox_YoA>+5AYDnq`~VQ7I3~PHv0n!OSGj9)4e3pv6I+V}>R4_G z@Rglz^W3KYqx%E#dSnJj5f}G=gi~FSnr2uW>oUJ|^v?LFQ@(TGaX~aI>efQCOJ@tNHj^oGY+{U7<>_}yhdFX5 zpjnd-C8AIdBHixT7Px4jv1;7M-k;J$9&ScB8X(Q-q3N}+eKpW=c>aCobh>xd)Bwml zt$9p}v04*o|6{24Ue)S+LJ97p9Z~`V?ZvnQDwmpA=DbEaGI|)D%A=$F$gRBIJtvI6 zW`eQr#CLCWr3`6WQd3LjU9r>=aLhj-^jcqXeWiBmDkt)tNr~-Cz*yh)pj7MA>Z{!h zTm!q5m|?n}QdT|0KB{ibu}@_Ev|Z>5vSB#v#;VL~Bqvz+iA)_f{@;w#1-^G;2hI3(aN+PPh2tDq?;&36f5yye2&w~qTQ zrre=&lD!b~Yq`aKC_}q2zEgd}`MNKRztsgOkd_X;7T27RrNbJSo3f8#`YaIpkPqO9 zAH4~R#pXA=oS!+R^;EmM8ERbe(F%(p4sosT7o=uOV$lh%AC}3G=)DX)X;hQ)L=-Og zpwl;|74ocV*()!e0_uS5&48uKNUrh5b_8MM|HSILvA-Ea`y4?PR85)i#oN^{7W|8s ztbrpVLH>gsa%W&kBzHI0;D7fL*z^9~ao?+fPXTTIM#-!ZbsWJLJQFyC@VSFS`^c1# zJv;e-8u+`w>)}hhBkavR2*FXI??0m^DJU^DD?WQaDu@*pOQx47`|s}#>Zixx=^BWc zv)k#n!Tn7(hrcBYu%Ny%PnQ2rPd6GaQPPtV9q`{Tx&(nInPFY_sS*K>4zz*!`47IJ z0zM1*@c*nJLf$xWEdOK70((Ni7^`2FJ3T<4bSvZ!9xBSuZ^2i8dHt{c_rUS^kDq)W zSUKRiwUFaq~B2MW`bPD;603pPvDvGq}Dk7Bc+Q;~?r{SFbs|v8Vrw zwVVwIj5g?9LAxLS~Ymk%#~X;t&S_LoXpg z&<5fS$JeW{FW#E}jRQrIztX)gqCmdbO;13f1Yu&o2DVq$V0YQ2cLeLTnV+@#=>Gc; zf_T?!qGcd%Vy_{1#tYn+_kkq$Zwl}Q8Ki*&ugqZJRg$h&VoUbFA3UQ5EG!)zKg7(c zBm{I(vOeRK^lc@`22sKvE}1wl(0Bv91rbnmRiFQu#~ZYOOR#@?dpnBw<-clulwNxQ z+%T%AUG^hb8J+7Z?|9R%HiYMWuI6N_3Hsj;2*Mq;J+`K+m-Zlf;CEn-R+m7THv$QP zL|NPqc@Cz(Me?<RI5l++T=(ks@rfGEET|NS((-{B_g4z*)yc+WowDp#ZTfd=8f z`(+=*-oZ;H1SSPWJN{W-RoOuJ3BChhsv5~NSROuf9wK{=?*R4h%D?-a$5iR+Q%lR| z8h!*NPW$)4=JgkJPMJQe)BwiI>pCUZD*o?g3a&xCVFkA1H?1*{z!Ie4XHA=PD1R!g zr=qKsBbBzF4FcBVzbmZ{kKG-<26l*}l+WnERDzt#;`!dNRJ;4U`jR(Y;jESU+2LuKlMloxBvdOQntdo z_wRRMwMtFjxQG}@{=3_VA6T!7@gzS&7^wf>b120!fOj+K17JU@ z4&v8_=o-KOn?gq5wB!5r$?BIc@;t!r>EDe}_pUi|fU#J`2Zz;%f_bkF2227kQC0F6 zNyVUdmljSXHus3K4Ub#RKV3PN2NEi4Hpi^<{B``6D6Ty^#=X^m@!@m%v7yJQu7LIAy+22 z6FI`m`zOxo%tqlwe!Q`ze@_RH)iR$i01&Q$h|I+&ynBbl$i=D51#&m|eB@#dcX39s>;7tuIX4gTp>Adm1GK!; zXcNme?*S>SNs4O*ams}=8noD09cVxV?u2&e##^LYrVQ; z9tGkqzApFS(>=vi4q@iw5<{h#gIJXJp5~1i=txZ+E>3s^0EvlY?W660 z`-cGml!3wFmLrH7lCjexe84@t2SfgCL7L8Ol{(G}QRk`vXBig8Crw^Pd7ozx=g0cU zs^CR$mNapaZudZCr;lg0fverGwF9LF1CJIWETbA1?02t2tnwhaYxWT>?+4%CXuGlT zH_21_?AYq!xW8|XZov(UD|&bFbr*zM#6{*B!rFOqdyu_3iuM%@(r14qJ+EBh$6ZH31#dA2_d#?bV};WoJPCK8JQVzTl%Cdr*Fx%x$BQ?%O6Bi@W$2jqORJ zD+hUB+J-j65JSt&#PhMiGcAL+n)P5!FXQiRjBOzUr1I7~{vu1mq+#jdFib~MJ<>l5 zjm$&h5(_Rawq7yz0PgVEA^HS^D`G@4^8R>^^pwdsD$Zl+4vWX!C1H6)jGXK>+z-I) z1$4+EYjEaF5+ug~WCSSD=-lQaMsypKFNNaX)1CQz07P0@skA=G1pT+tFpqi^DcYOU zej*+;=GxC+zVM@z=nd$w-f3F8bA{`sk$(`%Z$m0ws&9z34^~UN8la!w@ooP@@WVl= zyWJh0X@|kHLGFu9`P+>m8-7U`_cOG_cIU%g4>t75@z$OB)A>E05Vx;-g!4TG&WI(VwTG#+5dWE~;O`|ckuMjY+; zdg5*@O;s98%G4rsXffHd%`=dYTH~H%m}_$7%%5=fuduf?d`By1C=LlXT$sWMlSyP8 z|3**+T<*b1>y{J>Eui*n60!$5-A@W$te_CO#!?AGt^t&~jW8c3^KhWfC@jOs_U%(4 zk#Y&u2M|jKrJnRn%r_i8M&i-6TaQPjcy=5DAq4eeU(dx@;{Iipt)BK@w<-=&ao;#= z%SOELY@m_pd6L}rul#W_^GTL62CgK>kk?w_!)h!a>t_bb^OBZzvTRMy;^`mA>$zGS0U8qV^>szGSa#=APM@{piS3t~_DLxt+-u0Wfc7 zy!lf2)+&~vJHSwygkTN7^)rC2pTFl`rfPNij=Ef%cl-JmL~}p7@>fb`ARor~ zoi+l=Cy%g!$6gGAb!f9lu-(4eK-Qq07cOnMyWW1P9BPf*^*DDU&cx(ap7Z|!^F`X> zJNInA{BsF^?Z?~XsMm=aLRTxjWIsCx3((+lJ7~g>{7x5kkJN=f%uKkoMuH2#v1JJj}mTe=v5_?DqoFx(1T7~BkciWPO->X(RgC4a@SYt2Ei<$ldZ zO|bCL5E)pv!`pPxBV7rz9Dxd`<~|H>(rgHJk3}D5VqxcdnQ1A(flwESi1X?LFh@0X z0-1mkXcS@sxQrkK%#C0IJr=-$egsrjrX?-Jsl+3p=8m3=RNa5+#j^{$dZ}Q4!rej? zQV)Bo#qT~A!10&IoJ-4Wpf1|w?uIYGYiF@2|p52`$pExF?1cjM|3v4W@V$mnd{Y4i0p*$Gf zyW`p=xrMO^kwKv;hxKNtR`p+WQpa)<@r!#eo+MhE=~5OA-c^>{kV6LSvz8;uz8UGR z>Dp5i*-q^p-dnyz0wtW#*bf+AnW+^{fc1f+Q_Nh)e&nJaVC`T3nz`{?!>z*NYO?Th z3dDnx%`<12uT7dqw1crTMJwou;E|HOnYi<5VKmjhKxGl+b_|mYkThUP*u{wt9il<# z5DVPCUkmrABT7QKzXw7?b%8PS*)!ZFswbmpWEAASZvG?@uf6&b_`cf%JFtC)ngfAm zJC)H*?J-%2Ic&0&^z6HRGDtdat;pa`N^;h@3mIph1nRSV-z=}wm9w1S38}YnGJ8C# zj*wS#E??&})NxZ3qNsxb)F zH`h3}wP@s-nOlTQ@tEp0lYc5yVS3hSm4$6?a!@bXLw-a+O>7?gi+Xo;Wf!bWD)Yr4 z8-$u+fbNqGK5iG+?gp1(Eg3Ba$0dW(_4VV;)Ql z@lDmDN4&Jf(CiEJ)c2Ik;ufg-H}7wEKm2wt(OoBb%RQ%G3SP*v^h*jfvOq=1#EPQ# ztq?Dv-Mg6yBkj`I^$Z(GXxk}7Wsvn!2U3_cq~PyVthdw_t{it%yQK#|Hyq>si_qyS z)f|7b@dN=wL3&&s_lB*^cOlW`5H%2`tI~ofJ>g$l4-i7^o;@?BfCzT$%%3hGa0`?dc~;kzh~Q2o4BV&HKWdItr@qw~Yq$^A?ppyjjl(szi&?_d zKdEdG%ID+G*5^&?J;hSN_G2`yUUuPY|KRb?zwggD$#L8N3fkvMH=pT6Pa>uR ziF0s2LQ-On+8c=c(qc)4#*xz6N=f|bPYGG?1TUmm5^{45b~Bid2cBG7#GC=}Rb;vq zs1z$zRT+`T@S3P!?ozZ0%#4y2wMGTyCQ#Xt>U%+ebFHHOe7vst(EFX3^_%DY-sidEUcDW_=FQfMSt1V@ z)|Ru3Z80=oGvLaEdG288H%)ino>i{VoBi8Ymqgqj|2z8#cmAb4sra+624pj&G9lVV zJf0c9%J`G-Qr5DgKfKLYJ|4Ya zSxmS2uzAFfI%rp@`pW|^?F9LmDlW(oSw4T3X5VDfATt-!1MeJN&EC5=8|-I`XUdi| zwr`CGj!9{x@fNQH?XUK5Hc9A>!)bPpn6}zReQ39&KA;Y*2AhZ_{CLEJht96$x@$Er zv^^it9xB*xHb+>Df~gO^%wvBrAV?qybfa8@4P?AM%$j1^NE9u$C>pH>PbCzdvMjxEBfG~n(a-XRo zz*5Gn^dM?D`dxS&4&?0TP5dz7r%#`)(VC*|Eaf5Q8fXi2LcU2aEINhDIPui^%V8^xxXa@U}#Ed`^X)ih(Dq#_fr z2b2n~kK~4HPwdeqxh|+1sFxY7Vhz`HbS zU(<0cq)?S>!<0gB^JX3USc#EPC_!LvVXQKAfV4;n*EAq(;+Q!H_?&{5l^=MWv`w#ph!3ItD{?PV$8bR2` z=C8RpY@gLj=Rn@^d4Ur!q4AFL1vJftzo|2i=H=kK5&Mx2%K^myMy;&+E%5XO;H4k- z1WH1Xs$WO}BC7@ftQgJWeeF*1+7M)2HY?Jua{<8R@4=?&)OF z73wb>E&Xs3pW~lp6mLMun3Uz3YbWtJSKujZmck+FhQT>^Pl zJ#trd$9IekC;)?)M0 zXanLeTgMrAO|Zocqwh*HQE(k6jbXW&sQK$SNd){`3;LsQ5JF=2Z3mpoXg?c#>N_9O z7&-0x@L;z1*a`3Xy8^iMWXCag$P+%bUaN2TuUw0Bh&>RPzCY{(##w`Df_e@=I;t%9 z23CaCL#Nc5mi~m_C9^bnVN96%(P5;`>ipm>bIqaJ5!QlCg7@!ecZlidaqB*lP;2M{ zHEoAOV6drp4S~n@8G!-nGuMJq&#?W1v&snWM;=>R^=-yI#4;YvXIRfbmR{e6d~8zaQx9$)_G-LW#L0X*^HmDHXnZ;lr)jur%RKsRns{7l(mC+?kDU$ehk# zgyr$b+NhX%+Su3fJVilfS;%Ck_@cj|_^ z;=3+FM!#6doTy@nisS5<>_!0J{F`>ZCE2xk#SXa75!t!(aZT86KpG`GCKZP`-$Ha2 zz~fU^F?wCb2qeIOR;V>u)`5U<32A?!m8<>!$y`uPmlp!PL83Xt>LBA-QVs1b$WO(& zRWq{jbXh(k&fJ!Yo@Ff{gX;?owpML@u+kk13$rn^p(yeyvJ&oVsdWH!{nl5fp6J%R z7QvWAho!#HaLRobhtdFxc=ehf#V-Jib<$m$cwZSvWGeYi3WY){A?R z)Al|Vok1sn&4)>-sC0;(od`$V0&icwV;JZ%Vq2vNdhAgT0;g`g^(Y#Pw_&|^|Gor4 zd2C*M+0xxBJ)8u1sMk)5WvyP$n_$WydDw_z!S|XHSr^|`FeP2pZ00zCKwfy~32-8YRZKI{A)-Df`WoAbTeagO7SCCsKvu|Im>STY6b z=IsSH6*afp#*il5AvJl&;Y+a9_$I%Hm{)^_BKx&@tF3ykI;`{j>-bY3#p+o#8X5=q z+@uMYEJ4%Yy-39(F$w4#re-^7{LlfMnN z-6_heHuh+@!)|hGpvfP2VdzM>F*we+lx7Qt1Z-wEx)iy}4C%O$>+wAw@3e9@6Fi-i zF|_)9j@=u%eVrbZ`6zmb@YTB&m-jEMt*w8&;vk5}rzn;n9-Rj51Na+N%NWb^tgdt# z=Z{dW^ND|@Vnpj#DgJo`c&tmvuO}?hz{UaHO7!k)7D%|4jrMYEkv90EAG9or#| zl_RAHxF_=l-l7HBH#bNzN6btF9*ih~MIWdJ@sO;Q>yu66-C#37RFn0%t3U%2g{iQw zj^;dq-m2B|ummFNDFx&6 zc*_rUkz4tS0!!_N{-Z!#J_dn;k&20Er!#3UAu+21w0A?^5Gg=GvUt+o)}~%a5i$r` zSVrAjCZ-0mz5dzBNkeq2dI8~Ib)lD!V#0{muDH{S==of$U|MRo;|Pl ziHM1;3x#+J794S9SQOh2fGJkt_X&xSA#cs}%%T;C+I_%y@U=9!2%6pF=f2$CO>}4k zQSLLLXOA`f{uAh4mk@djG`z`h#EMb4%%84H%>2rRkTxq)DTz{hc*Z*Cq)+LV!pv)^ zR?!aE(NolulM#DppN)(|K+yX(9^CtHo*u$>bSo3JGWAAqJ@0MNEx(Rnm)hgyKCK9W1#Y^%>mBn@$wXK>mxYcI zQ%Rm$jSkC0>>}MC~X^64$X;W~YW|8)kP7N5QF!)_xcZ zOzsy-fIfaPb3iirh;y)ie4Iwkl^}iGjRBa0TgUkkb|-<4T7$(FQ4gH-x|ExO6q!;v zhhoA4$ga$u$dCnK2#pp{yp#uZQjb4n)b6oqdw++(^lucW@HRWo(X2M z3yHb&M;`nS;>YpB`c?W-hfK%GhaCg%R_SBOZI}(I-dAZWmNJLDpy>qXcUx*R4)imr z(Brp(_OJC8!ZK6sl`AllwVtsxTlHqqPg64M&U25~yodSU4ArtvY>5J43XRu_dm5Pq z=98)ksAFwZXYv+7^tomO(gBwvL(996Jpz~AY5)v4{xzU`v zBorEVjd5=cY0v+^p1wPt>i7NsbsXa@s|eXt4v9LBA|anc11y-~04={C!*%Z0bzk>2p4UoK@Gjghs-c0OxW6<}7iftb zF8!p_rIY>Pb>44Ks^JmXbg(>eO-*dZcapTNs+MQ#6yt`iWh()H^?CN@8-csj&np$0;%!0#q z&*B*a?-Z8(vAsx2a_jXKYONOo{n66aW>IhlOHEkE2P@1CN~*)&F~-Th%R_dlQt)ee zTIaRp{3GWQk-bQI0l32eJ-{25Rfg*7k5i6ZH?k`t^>~_GJ|7P7ZUtaQ-`I6sO}GAJ zeP!OX>^xpYJpf~#cAd4c!}Zwzmx7R7m)W*Yp}Sqz*01Az)bbm1hhE zA!7A^n>18-?mW(DsmeCYNBt}J(J0C~Wi{LDrt2jUTj|vNQR;?|b<*Eu1ma$r5TEit zukISrT^!~W76KmQL(q!6yo8YeJ4a*8Xi*<0!(tWmG5F)Un*H|I7jJaZ24q`5bNtxT zf0FjxNt!0(`tuiG7KQjFyZk z*O%fAZ~q%(gxMM0JH(-~#Va1_E{^*iDa`pRmHh;JNr4*|<*L*4!r0PG7lXj$oZUaw z+}vz}tVcg-{7*9TvZWQ2p#l%4gcvLZ-Tcek!bO<>=Ju;Z9D+TjDly)WQTV9KaKNnH40S}e|k*DzN8=1l?I?x{E{&+YAP=kaU__=dD#vbm}0-wGG9A665|i8b+H zP~oxU(8Xk_-{#qWjnoDoeE84FDv&E} z(h64Yg7t!IWCg}nXne>+9S1LETo%iCGP! zFyrq*jI$S1@FwJb~;*Zh@qd=HVPa1v)+2yg(V%6iu3;FVHW)K{oeu? zq>_|)bjcCI;YC0Eh2}k-I~blpDe!@J#?XP_nv2ESZfUxS(asJ3@B2>0z)D3$@cDH| zd$UEpY)kZ3Jhs*@E~~ROL==)uUQ-wBP5hUd&9@9-Wn~3i&TaWHwsiLv>(X!pZfq?Y zX8X|~uA;h{A9o}wZjCzuRS`;!+P%_WGa9r+m4t379sQ0P$R`Hm*H~NoS&d{3#p~|bYH56_qPwX*siGQY~=^c5uCUt?(XU;0x0$V`0=BCpMTJ45l%h;m%}v0h1PnhdhB-H z&(wz#O332!YALFsd$dlOi^7BsmAV?usR45SPA?L-!4{s$qb3!+qi$MC3Ja9<-uJEl zKZ9m~XF*(Oq|y`-)jbN4{H64}RdbgQv$E?QH3E*ERvy6j9|T|Fsp>_7JAFDrOm73_ zP!LI7J5`~=J1sp@USWyLil=gbYo0IHBt3BGC6P}D52~1w`Xs&<8>*g-5_n}N^~Pl6 z)1Lgo**>rO1vMt4?0}SI-;(b3a?W1=i)}e>hHtY%pRC)%ETvK)Xb>BOBA-4l-qR1E z41o}MMv<7uvN2dmg+>LNV1m=5nM$rW7;ekqMMCq5reX-4UE1A%+}f!0TT!YCwH+SV zvlsaoeo(YNhd0bl9ysD>Pqo85DR!lSEV=)oKzeU?56g+nB;OG1yg5tjKNPY;BpAF2 zMP<_|n!G%>`cP*}#9lZH{IUNbwNYq!=eLNIg5Jo3F}Zts@GF}8GU$gx+mAsw?S57q zj=l{Z5$g*1(h>sjz>qtkh{U{^TjKNNd?jqswfJe%=P+yb@h2D^6J^_YxWH~rnNoBC zSGrwb|IY4)vr@j3VDB3GNI;?!C^Q-HSS1!lZ)j<0(Ob$fwsw`2{xkMP8Ft8L80$*p zg%mZLTI`$#X;5s8&ApcBT;9gR-gJ!~V&|xwvJ7iy@v)$k0+5!PlQFQY>9w`BS^t&0 z+rpO5sgz7GSt(1jdj|OqnQX90D-$F_1ixPlf7S0Mo*#nLmo|=)wIIL}xn9~Gk1eR+ z3bx(ZC7rDhn8y&5a2=fMzMGQ0`)XS*rZjvEA*nX7kX6|e+g0~l8Dm5bDdvS*uk+Vv zZzXWOv8z9Wu`c}_8Z(^m8Ixwm`C{cn=k)&Ws#$i&blL|AAqdiYSJ85GRGZF>r9xLfs}h3N-a+9#;dlra?PETyHYL!S`T7CKUMVNrZVL;0Mf zTG>L=i`TIEhk0|?-+5}nNf9d}w(81Z1-!{E6Div}PcVbT*wl{f5<7dRRGBYHNCI5Hek00dqE-MG&}YCg z)Kwr2CnuLk2nuDNjV7s@)1VaMB8xzcUj9UxtmakHC|J|5{9g)}CIscYr-XugUkhwX zYKkK3PYd=H2j7FJ=OQLC2|9O`B`))c;nn8AF9rvJGO4F;26SlaqU^ zP62tfD`2@iet=(4@B{^l#>`d7zs&f_NqPm8504A-izNJzVp(F|GZX^DReWo3m3ZEJ zG-wSLrfcTVyeevn_}7jUN;Jn z&44^)V{A?T25#if!=;a#6- z>2$*m^4>Q_Tw!bn&O&N%aPUJ6Z{hFc`z%d<Mw6ZG);9W+eG@fy1E{s%&}U3fBP;gtpeyGtD$r;QWEAMz35%Lo9PZ( zCq{ucW=2R_BvO>?3hRyJ3{5+s&9j}P5ZWeAwj{}>e_D{wcNp7kRuEUfi1yEstL!m^ zsp`mMAVLhd3zX4f%%SGJCYafzBDfREwXdw3ggED#>gt#^ddu~~d&&1v&qHnWyDho> z#jix)rubb}igRdG6@`?Az{^IZ_H@uD5DjLE!F32iD{A0z1DmD$foj^*7VcO#pdciM zCon70#oR-3_&hnKCsq};RLB-;Ytkh-(OL4d3l5Q=VObPa@Yi>Mdy*yT-q9w<$AHI$ zpN#v!=iz>n?K<0yc|E84nD+--Ja8mt=+DH&M4im}Xu30e$7`Rs674v#O&{=%)n6KD zA*3cxo#?cw=#t|yYeFHvv%Py~n@s5b|A<|cwAQ29yXss|3%J)4!`PBzUl9|^-6uWS zXfl$?l}Ir0b`%3EQ?t%Zm73s17)F5&YV*aZYurOv;hXq5yu`ude5V=FBrLF(Nyh;+ z$O)9Oa2}JCCRfDeOP3_8$u~b^iOF(4LHQy^S?3j_^%JLbQ1?p~y}IuTX^xC-fz-zvU^j zy{@3po5%=RGPxNETNn6{GLz;*U|b>6^2eW#U@(Sh<-1FdaCHq*yN1SkrCh3 z=E91!uVBskkBt(&fLtFv*~f{Ir;5~jhUH)o$y};fA#XOdUxSqq(u?;sauS$Zqgx{Y zAy6HwJ{j{Oz^3{23F>A%Q_GS~u{zLfPE{f`A*j1WnX z@!NTEy^BZKmFVpR=;pW40`;!W4@{GK!03i1J}s+f_3sX1#1Wn(tzZ3b=t%|b;?9SD zbj78pH*aYBA*f%tep)pJY~##GJxW3~^QeQ-9W2j$G$`&x*PHd( zEX>{{@yH*>yINIUN-|B$y_qN>13^*ergE>(QeZFiqaVD3&4+Glkgb7#y>73A2*IXn zn;>B%%^^Q+f@`0RrHRQ%kz+S_OZu@uL0!FfQXV#gNNNQNs(<)JUN0r1B8u>e&hL^H z4>WPyQI9I7%hpC5tNtv9{O5wWs`yEjEg@T}iI{nfy2!FUbZJ+5|*; zny<@TCRmj%nxqmOb!bSJ7@LQ=Xyz0j!yE3c zu8|cfR)VF0{ygaKVq;|;cL+{>g_2N!*HCJ54Jh3Pd`1j_A?=78n^H+!nt4`9+>k=4 ze(bk9-m!CwW$P@PWdhI8nK~6jqR!bkNX1qn0{#c$l z_M~NWFo!oxK^D-*oNn6Xd^yGcDJC{)A2#H?PYjrEz zhReQ$a22h0+?rIjCY5KE8Cf3&!+ky2?#YukeZbmW)bL z*D{7f?r-li0lQpQ8&7`Pqyo98gDdpX$n{Z3wtacv@5_M#k*QunM8DK&uyn* zeqkm8&+~p+r`^O8G8KlA*}(nZ6n2;au)(g*tTNBUmvX4>HtPh?a*l~4=4kz3)CUQ= zO;Hs*diosP1RN?&qBGhR|NGm=e>v`ujaW@Gq(;ik9?B-TDY;HSV4=9gOrNV}|1fI2 z;9usp+bgamZgD^{WC1urz2s~t5!EFml>m7$XLE(6q|+)Q`0fG$uE@KGY0kXxD}@|s zKnE$a>8fYZ1tjTzzj5;>#*8C@b2%R81Q3V!Aabwm(_{n${+#Av>o<^iM;9F(%`v4m z`KjHdIvkiOQ{u|*kZoDH14hVUhvDxN8cjR5jLL!-8^o-F++sxBoit+zW(*T~2 zqkaEt%~&kt^~KRfOk7V}#}=LN+Jy;0Rx4)7dkNq;kV6mY@ zD^k8hg39SMSI~D{9+ixs(748O-2R1OSvyns{Ysv0YcBSlJROV3}}6pcP;+F~O61DqveGcT0g z(yZ`IlI9oQCFd7-6MBBgQUJ9A^ilVqS7X zN*)Ii@@u;$9_==D;p!3CK+6nt>b@oy*8O7veeH()XgD~@HH4N5H7 zPtulR<>`H6Np0CM#G_F$b6~LFy>PAx1H0)a)qpcq=<0O9;CO%K1~5efFuciwJC^hS zc%z~p->Mrk4Gv*kd6<*uRSVu%b zqql)MI{ShLFrsigMf&!L@S9R~$3x~p&I)eu5Yb(&kzMAj$WtEOmW{M~s5}61SX1L5 zQDijTh))_jkO#%SqoX5_#Y_nYTc z&U@pID4pe8M!S=fM^aZhSr|zd@%3lremD23;(;e1#PQNGeVT65iVBJyQUO9plj}AB z!p0-vGK7zRHh=Xz%ngP?iLwUw7Bda}<=Nk=>ogrO3|__Gm`NSX_!wCOw^Tg}Jo=ow zUd?aWYH!pu4cpVb*jgR{d$R8L#h;)?b=_!WzH6zLY9g08Ae=<_H`FRY1kca@&8VRX zNyF{$OM}M9=+qZW%p1QTQKNOGp|kw8Q3>iagu0* zpm&;+`kLRWKhtBoKPvQ50aCNr6OA4nF>nTmW5e2L_TWH4=+@pKkvFa-gl1mNseXEx zPI%bX%?#NQ=I&%i(-n-#oiaH?8lM6|+xtfAyLaz4g2og7jhaY4)arv!)h{99J>5&vha$Yu&3hiP+T6Ay3c2*en z2#b>pFDrX5 z;uOv-zT&%~g}=wf?&fB101Sz_Je-t7YI8fa-?!eH4J%DI$XZ(q30PWQ9;4#2fM!v7 zDhUk@f0Rbb;q$XZ0j6MKcA6fVowDw;$q9!Y5Ds;3-g+CXw-QCZRbw*#$-I=Uq!7I) zbGQgSM6Rr!*!*C?Ru1q~Xi#&Ca$-3|l+2I){+yql->{zs+Vf1aN0kiolP|T_pmy53 z0vHntLGu>()&}<{p@CQb=5~cK?nT$T0#Pp_>5|2gddM(aMS12VyhsTnBE;9UJM4 zRKZupV!zPes^Luj z`sjBOO!kT2x|YWYswSGc#X@%GHGJ^xMtL6z$caLVi#o*VA?tvr^g3wU_|d(fP$Jmu%@ZTEl{Ofzgo zAob+B*~N3Fu#lw>`%`}~^L}gKJ~#}U#%(-TOpAi}57l~A%b(_#fLH&fuA=bW_M?J7 z12EYP#5N}Gg<@~$u_Nn0m9V8Zkz@7|?-byFLuj+Oilu8;t~p~EAhcR)C*kGWS15vH z&ld!I&6V*?|F#C6VMqwdaxO6PD_hvN82!DGcG==hSmCK*q)MP^8B}$Fsl`~|Ia!eA z&jYOw1D?++9wUI(M&FgN5fl)pF@#-8ssSJDXVegOReS^3nkOwRo0ENRIXixRY&p!W zl7Z{HX1{p7&>oF9GqgXjfmmlLOFxEyVR!r4{fA`|wC409y=SXSc)1~xfN|^ndx*{% z6HUaT31iE7$*GeNIeZ4fJmjYM=*%iXde1#PwtnUb?f--y*SRnl65#ezO3buo7PF#(~*{zmRw=0l{A1{z#)>iaiEqmg0FgO(l$tXXj(H9?a&Pg#lJ#r zL9zE6l`qFl)mu#LT0cJ|S1q9Gcr0HRyS+G?u5?lnUKG+l`vuS{yJ5heq{SwZUE$2^M%wd1a+ft!Znk!WMcF3;N&OZwf zVgu|(8&;Q5B&nJRz)|fL0!Z~pG&Y1OUslw|%_BIOTgANnQ)QeqJD$@p@o7m9wInF zzZ@dkF9)2l$d6FaVbXho6Cu&XFd#)$}p8&v!QP5MbI_H7px{HU5uF)0f zhoc+<_TYD=1Jpe#d8{YhP zXd%Isyf}^TCYK1W7}xBY+~e9FFC^20wCZ?SExwsHUa{26>e$sIA&pZ0>D2NX0{nYAe}<{=Subd~-52 zXH!$|g29(RADfs_LV}?r%xm}TjnPRfUEw~u%^plhZJ-E&(hBZTgVHYWc@9g%$a5`~ z`m#^i*w{);X^f%|4-b=k9ajiIcy1a?a$WB1>`a^3nNxa#vE5}m?n@Woh|172nyG8ZlJfa=h$k9) zTIM*a2-bo5R$aD3zgn^TqB8KvShB*7`)9B{U^PdQ4N zW4h)g4X#01b`0y_Uc$>ELE|8lho(3z5cekeGFL<$pJKgOD0aNWdT$F#d*WxWWkjE) zjpf1Z-d9=}jyBmB#ci4Ny&;74z$^!BYP>+yaQ(bY0AjWR{}x?KvsUi5n(J^Z@f=pO z8?&v^E;g`hcEnU?b_57da`ZB|Xwl7IWa=@@e;AC#?S5e5De6 z@Eq+~A%a9thEMznbLGe8EP<44zrxPzg~g_1_2bWVfjdkueDyxi?46yYM`}A+mwpdm zv@9w-W42l~gv{B%LJ6OWjjvz7UOAqTtAgk6x_^&av=fAa9AdW8i)$QEVl9W<>F4sf zBEC+f1kV2ZsWWdi=}3E(M&_xD2fK|LeOCVVE0F(7h^j=s->=%%fRbW0>s@bT`BZX1 zv*#Bz@WIoV#t+_twrS_2;`Ofr9a@ab#e_vgMKRz~&!@dlrmW!wGkLbxx!2uxnfCgi z9epu1*>S7Ag{zA;Am#11)n`~H5uTEq%nUi2Hw|3PNqGI==qH%vB@?>f>V+3M_pX8D zSqPRr5oTGV+g>;xQ6xC6bE7L72)lO6XXdC$vBq=8M?tBVT}X96L+{PO6w{q4A?hd5 zY5jK4#{1t!Tz7c^pGz-_cJ6@eJ&}}E2{Q7TyC}%aVdD)(q$}jMo>{vBt)+YSE8l9t zY32VBU!18~9QpC?U})`S9DI~p2`n$>0v%%fldJ$x#I&ynruYl|Q z19f_ET^+~qfxGc?X?jD8O2->Y3^?;@CW@bo!pFPWl0DPh316Rc+LmkNN=7rjQ$K}H zDy4o`fsq#bRW1T7OC+LC-l)52qQda=&1jAtfXM?)w{G3OuR9Sa@tu>jbc5CXtobec zjUSBC#TNge{KwtMJI$SFu?Ai2B}Lv=N>Btoj5lG%)7_%;jwIBh!<|#qlEASGAw9fp zv8lh8hKd&RuZs6b7z+vsF;O7L7HV32+;pY2LJYq7$V>(AwF!zePRNXAI#)Bbk~b@C zVY5@8!TkAL&jb)+!l9mbn*RYmd4h1&A+2b4JbQX@y^Gt%nNLZVi z1f|#2NXwfBc@r=8IJlssUE?Vt)>hY*9x7G8N1rZaJdmGTK5~;codJYxhOJwRbB&4q z(vIYHx%K6BxlOs&vV3~aR0+<%buo_^ldJ(XLq^6=l16bva#vVEGR+89i5ldTOF9vybl?1i}2tA1o-(! za~9_W{r&tL1^!8>p1wOyhc##6+l;nq?2ri(3=1@6EwnH*GdtW^Ka75n@<48U#?MVXI2@dVW6%2sN0qF1^xgf0H z(8-*JJg2Kp54vD3Mh@V>9$E$*!30><_uNAUI*8KBhWDuJpu+XgLR9Jsi34ERLdItZ z5$C{Mk4-SF19y~?k}_89B6b9ul(xwu)zw`-5AUkr_h~<@R_8=AWZ@Vc`fOy!J?$K) zoDvL-k3{^6_#>6-fJO;yjS9Spd-NW1^DL-a7V?$EBtOy7 zLuuzaV-&RQv)|mQ62;ZpOf3KJn%5mB1ugIEYTEA37>=;ET!|)(?dMV2CM1t>0au^2 zU-|!(vxv-Ozh3gE1r+2*46#WCS#~fX4qI5^i*VC$5&Cdi_(6ou7j1Uu8)EdZYGXz~Q!EP(*K_EV|3hGN9J4<`0r+7WG-4MvSIN(rYTVh2s zfA{9q#NP&7jj)99(VmA)uOkQCJD;a92(SBnYx+er(6DQNN31vb1KYfuy9F?n9Z!Z# z9Amj%*(Rvuf9|fga8|OXxC=dgS%`LbFEB4JulUKrtz2rVqQ@Yc0zkD*FdqdtG)P^p*C$~7$D?P4R(Si}$A^Bq zGrMjw!dCb|eJ9&J>T}351R3WSrhu+@YuIC^JF~TE@v^*7mFR~STAOuV!;Loz+=Yfe zfZi7}UiAjUCffbMa6RDW!O2`1ui9MVERM1AvwqUgaz;q#q64-yw?75LZI?I9pV}ra z%vMD+Bb}&^?Lx%N>vU$af~cgP++B|3zxmpPI@(hhfhqeR=%&F#vH~(+o5S=c5biO89WQl=3>kH?A!bg!a2Ys^8YSm8WVM^nbi8 BhV1|V literal 0 HcmV?d00001 diff --git a/plugins/persisted-scope/src/lib.rs b/plugins/persisted-scope/src/lib.rs new file mode 100644 index 0000000..9633165 --- /dev/null +++ b/plugins/persisted-scope/src/lib.rs @@ -0,0 +1,267 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +//! Save filesystem and asset scopes and restore them when the app is reopened. +//! +//! ## Cargo features +//! +//! - **protocol-asset**: Enables `tauri`'s `protocol-asset` feature and adds support for persisting `asset://` protocol scopes. + +#![doc( + html_logo_url = "https://github.com/tauri-apps/tauri/raw/dev/app-icon.png", + html_favicon_url = "https://github.com/tauri-apps/tauri/raw/dev/app-icon.png" +)] + +use aho_corasick::AhoCorasick; +use serde::{Deserialize, Serialize}; + +use tauri::{ + plugin::{Builder, TauriPlugin}, + Manager, Runtime, +}; +use tauri_plugin_fs::FsExt; + +use std::{ + fs::{create_dir_all, File}, + io::Write, + path::Path, +}; + +// Using 2 separate files so that we don't have to think about write conflicts and not break backwards compat. +const SCOPE_STATE_FILENAME: &str = ".persisted-scope"; +#[cfg(feature = "protocol-asset")] +const ASSET_SCOPE_STATE_FILENAME: &str = ".persisted-scope-asset"; + +// Most of these patterns are just added to try to fix broken files in the wild. +// After a while we can hopefully reduce it to something like [r"[?]", r"[*]", r"\\?\\\?\"] +const PATTERNS: &[&str] = &[ + r"[[]", + r"[]]", + r"[?]", + r"[*]", + r"\?\?", + r"\\?\\?\", + r"\\?\\\?\", +]; +const REPLACE_WITH: &[&str] = &[r"[", r"]", r"?", r"*", r"\?", r"\\?\", r"\\?\"]; + +#[derive(Debug, thiserror::Error)] +enum Error { + #[error(transparent)] + Io(#[from] std::io::Error), + #[error(transparent)] + Tauri(#[from] tauri::Error), + #[error(transparent)] + Bincode(#[from] Box), +} + +#[derive(Debug, Default, Deserialize, Serialize, Eq, PartialEq, Hash)] +enum TargetType { + #[default] + File, + Directory, + RecursiveDirectory, +} + +#[derive(Debug, Default, Deserialize, Serialize)] +struct Scope { + allowed_paths: Vec, + forbidden_patterns: Vec, +} + +fn fix_pattern(ac: &AhoCorasick, s: &str) -> String { + let s = ac.replace_all(s, REPLACE_WITH); + + if ac.find(&s).is_some() { + return fix_pattern(ac, &s); + } + + s +} + +const RESURSIVE_DIRECTORY_SUFFIX: &str = "**"; +const DIRECTORY_SUFFIX: &str = "*"; + +fn detect_scope_type(scope_state_path: &str) -> TargetType { + if scope_state_path.ends_with(RESURSIVE_DIRECTORY_SUFFIX) { + TargetType::RecursiveDirectory + } else if scope_state_path.ends_with(DIRECTORY_SUFFIX) { + TargetType::Directory + } else { + TargetType::File + } +} + +fn fix_directory(path_str: &str) -> &Path { + let mut path = Path::new(path_str); + + if path.ends_with(DIRECTORY_SUFFIX) || path.ends_with(RESURSIVE_DIRECTORY_SUFFIX) { + path = match path.parent() { + Some(value) => value, + None => return path, + }; + } + + path +} + +fn allow_path(scope: &tauri::fs::Scope, path: &str) { + let target_type = detect_scope_type(path); + + match target_type { + TargetType::File => { + let _ = scope.allow_file(Path::new(path)); + } + TargetType::Directory => { + // We remove the '*' at the end of it, else it will be escaped by the pattern. + let _ = scope.allow_directory(fix_directory(path), false); + } + TargetType::RecursiveDirectory => { + // We remove the '**' at the end of it, else it will be escaped by the pattern. + let _ = scope.allow_directory(fix_directory(path), true); + } + } +} + +fn forbid_path(scope: &tauri::fs::Scope, path: &str) { + let target_type = detect_scope_type(path); + + match target_type { + TargetType::File => { + let _ = scope.forbid_file(Path::new(path)); + } + TargetType::Directory => { + let _ = scope.forbid_directory(fix_directory(path), false); + } + TargetType::RecursiveDirectory => { + let _ = scope.forbid_directory(fix_directory(path), true); + } + } +} + +fn save_scopes(scope: &tauri::fs::Scope, app_dir: &Path, scope_state_path: &Path) { + let scope = Scope { + allowed_paths: scope + .allowed_patterns() + .into_iter() + .map(|p| p.to_string()) + .collect(), + forbidden_patterns: scope + .forbidden_patterns() + .into_iter() + .map(|p| p.to_string()) + .collect(), + }; + + let _ = create_dir_all(app_dir) + .and_then(|_| File::create(scope_state_path)) + .map_err(Error::Io) + .and_then(|mut f| { + f.write_all(&bincode::serialize(&scope).map_err(Error::from)?) + .map_err(Into::into) + }); +} + +/// Initializes the plugin. +/// +/// On setup, this restores the filesystem scope, and the `asset://` protocol scope when the +/// `protocol-asset` feature is enabled, from the state persisted during a previous run, then +/// listens for further scope changes to persist them again. Scopes are stored under the app's +/// data directory; nothing is restored or persisted if that directory cannot be resolved. The +/// `fs` plugin must be registered before this plugin, otherwise the filesystem scope is not +/// restored or persisted (a warning is printed in debug builds). +pub fn init() -> TauriPlugin { + Builder::new("persisted-scope") + .setup(|app, _api| { + let fs_scope = app.try_fs_scope(); + #[cfg(feature = "protocol-asset")] + let asset_protocol_scope = app.asset_protocol_scope(); + let app = app.clone(); + let app_dir = app.path().app_data_dir(); + + if let Ok(app_dir) = app_dir { + let fs_scope_state_path = app_dir.join(SCOPE_STATE_FILENAME); + #[cfg(feature = "protocol-asset")] + let asset_scope_state_path = app_dir.join(ASSET_SCOPE_STATE_FILENAME); + + if let Some(fs_scope) = &fs_scope { + let _ = fs_scope.forbid_file(&fs_scope_state_path); + } else { + #[cfg(debug_assertions)] + eprintln!("Please make sure to register the `fs` plugin before the `persisted-scope` plugin!"); + } + #[cfg(feature = "protocol-asset")] + let _ = asset_protocol_scope.forbid_file(&asset_scope_state_path); + + // We're trying to fix broken .persisted-scope files seamlessly, so we'll be running this on the values read on the saved file. + // We will still save some semi-broken values because the scope events are quite spammy and we don't want to reduce runtime performance any further. + let ac = AhoCorasick::new(PATTERNS).unwrap(/* This should be impossible to fail since we're using a small static input */); + + if let Some(fs_scope) = &fs_scope { + if fs_scope_state_path.exists() { + let scope: Scope = std::fs::read(&fs_scope_state_path) + .map_err(Error::from) + .and_then(|scope| bincode::deserialize(&scope).map_err(Into::into)) + .unwrap_or_default(); + + for allowed in &scope.allowed_paths { + let allowed = fix_pattern(&ac, allowed); + allow_path(fs_scope, &allowed); + } + for forbidden in &scope.forbidden_patterns { + let forbidden = fix_pattern(&ac, forbidden); + forbid_path(fs_scope, &forbidden); + } + + // Manually save the fixed scopes to disk once. + // This is needed to fix broken .peristed-scope files in case the app doesn't update the scope itself. + save_scopes(fs_scope, &app_dir, &fs_scope_state_path); + } + } + + #[cfg(feature = "protocol-asset")] + if asset_scope_state_path.exists() { + let scope: Scope = std::fs::read(&asset_scope_state_path) + .map_err(Error::from) + .and_then(|scope| bincode::deserialize(&scope).map_err(Into::into)) + .unwrap_or_default(); + + for allowed in &scope.allowed_paths { + let allowed = fix_pattern(&ac, allowed); + allow_path(&asset_protocol_scope, &allowed); + } + for forbidden in &scope.forbidden_patterns { + let forbidden = fix_pattern(&ac, forbidden); + forbid_path(&asset_protocol_scope, &forbidden); + } + + // Manually save the fixed scopes to disk once. + save_scopes(&asset_protocol_scope, &app_dir, &asset_scope_state_path); + } + + #[cfg(feature = "protocol-asset")] + let app_dir_ = app_dir.clone(); + + if let Some(fs_scope) = &fs_scope { + fs_scope.listen(move |event| { + if let tauri::fs::Event::PathAllowed(_) = event { + save_scopes(&app.fs_scope(), &app_dir, &fs_scope_state_path); + } + }); + } + + #[cfg(feature = "protocol-asset")] + { + let asset_protocol_scope_ = asset_protocol_scope.clone(); + asset_protocol_scope.listen(move |event| { + if let tauri::scope::fs::Event::PathAllowed(_) = event { + save_scopes(&asset_protocol_scope_, &app_dir_, &asset_scope_state_path); + } + }); + } + } + Ok(()) + }) + .build() +} diff --git a/plugins/positioner/CHANGELOG.md b/plugins/positioner/CHANGELOG.md new file mode 100644 index 0000000..f4710bb --- /dev/null +++ b/plugins/positioner/CHANGELOG.md @@ -0,0 +1,187 @@ +# Changelog + +## [2.3.4] + +- [`f8053e65`](https://github.com/tauri-apps/plugins-workspace/commit/f8053e659e4ccd85c1f52833411ff8417cbc5e69) ([#3527](https://github.com/tauri-apps/plugins-workspace/pull/3527) by [@Legend-Master](https://github.com/tauri-apps/plugins-workspace/../../Legend-Master)) Documented Cargo feature flags in each plugin's crate-level documentation. + +## \[2.3.3] + +- [`4be76900`](https://github.com/tauri-apps/plugins-workspace/commit/4be76900854cae3dc91363dea71b54505896e928) ([#3449](https://github.com/tauri-apps/plugins-workspace/pull/3449) by [@skkap](https://github.com/tauri-apps/plugins-workspace/../../skkap)) Replaced a panic in `calculate_position` with an error return when the window has no associated monitor (e.g. during display sleep or monitor reconfiguration). + +## \[2.3.2] + +- [`c0d64bf7`](https://github.com/tauri-apps/plugins-workspace/commit/c0d64bf7d9c0f2c8ed1d2614745e15bbb3cde6a7) ([#3420](https://github.com/tauri-apps/plugins-workspace/pull/3420) by [@UrsDeSwardt](https://github.com/tauri-apps/plugins-workspace/../../UrsDeSwardt)) Removed panics and replaced them with error handling. + +## \[2.3.1] + +- [`93426f85`](https://github.com/tauri-apps/plugins-workspace/commit/93426f85120f49beb9f40222bff45185a32d54a9) Fixed an issue that caused docs.rs builds to fail. No user facing changes. + +## \[2.3.0] + +- [`f209b2f2`](https://github.com/tauri-apps/plugins-workspace/commit/f209b2f23cb29133c97ad5961fb46ef794dbe063) ([#2804](https://github.com/tauri-apps/plugins-workspace/pull/2804) by [@renovate](https://github.com/tauri-apps/plugins-workspace/../../renovate)) Updated tauri to 2.6 + +## \[2.2.1] + +- [`f634e524`](https://github.com/tauri-apps/plugins-workspace/commit/f634e5248ebe428f8305a59f74c13fc15147fb8e) This is an "empty" release to update the plugins' source files on crates.io and docs.rs. This should fix docs.rs build failures for projects using tauri plugins as dependencies. + +## \[2.2.0] + +- [`3a79266b`](https://github.com/tauri-apps/plugins-workspace/commit/3a79266b8cf96a55b1ae6339d725567d45a44b1d) ([#2173](https://github.com/tauri-apps/plugins-workspace/pull/2173) by [@FabianLars](https://github.com/tauri-apps/plugins-workspace/../../FabianLars)) Bumped all plugins to `v2.2.0`. From now, the versions for the Rust and JavaScript packages of each plugin will be in sync with each other. + +## \[2.1.0] + +- [`4db62635`](https://github.com/tauri-apps/plugins-workspace/commit/4db626354c8e29e37bedcf94787a8dd36ce21c55) ([#2076](https://github.com/tauri-apps/plugins-workspace/pull/2076) by [@jakobwesthoff](https://github.com/tauri-apps/plugins-workspace/../../jakobwesthoff)) Add `moveWindowConstrained` function that is similar to `moveWindow` but constrains the window to the screen dimensions in case of tray icon positions. + +## \[2.0.1] + +- [`3c1f3874`](https://github.com/tauri-apps/plugins-workspace/commit/3c1f3874f4c828637b3aa983cba13c77427faf58) ([#1911](https://github.com/tauri-apps/plugins-workspace/pull/1911) by [@lucasfernog](https://github.com/tauri-apps/plugins-workspace/../../lucasfernog)) Added missing permission for `handleIconState` and fixed its event processing logic. + +## \[2.0.1] + +- [`a1a82208`](https://github.com/tauri-apps/plugins-workspace/commit/a1a82208ed4ab87f83310be0dc95428aec9ab241) ([#1873](https://github.com/tauri-apps/plugins-workspace/pull/1873) by [@lucasfernog](https://github.com/tauri-apps/plugins-workspace/../../lucasfernog)) Downgrade MSRV to 1.77.2 to support Windows 7. + +## \[2.0.0] + +- [`e2c4dfb6`](https://github.com/tauri-apps/plugins-workspace/commit/e2c4dfb6af43e5dd8d9ceba232c315f5febd55c1) Update to tauri v2 stable release. + +## \[2.0.0-rc.2] + +- [`2f7e32b5`](https://github.com/tauri-apps/plugins-workspace/commit/2f7e32b5e07454d6c0cf3ab03f8af8da74c4a8a7) ([#1822](https://github.com/tauri-apps/plugins-workspace/pull/1822) by [@jbolda](https://github.com/tauri-apps/plugins-workspace/../../jbolda)) `handleIconState` function for use in JavaScript event handlers. This allows one to update the TrayIcon state through JavaScript and fully create and handle the TrayIcon without requiring Rust (and the side-effect of creating a TrayIcon). + +## \[2.0.0-rc.1] + +- [`e2e97db5`](https://github.com/tauri-apps/plugins-workspace/commit/e2e97db51983267f5be84d4f6f0278d58834d1f5) ([#1701](https://github.com/tauri-apps/plugins-workspace/pull/1701) by [@lucasfernog](https://github.com/tauri-apps/plugins-workspace/../../lucasfernog)) Update to tauri 2.0.0-rc.8 + +## \[2.0.0-rc.0] + +- [`9887d1`](https://github.com/tauri-apps/plugins-workspace/commit/9887d14bd0e971c4c0f5c1188fc4005d3fc2e29e) Update to tauri RC. + +## \[2.0.0-beta.8] + +- [`99d6ac0f`](https://github.com/tauri-apps/plugins-workspace/commit/99d6ac0f9506a6a4a1aa59c728157190a7441af6) ([#1606](https://github.com/tauri-apps/plugins-workspace/pull/1606) by [@FabianLars](https://github.com/tauri-apps/plugins-workspace/../../FabianLars)) The JS packages now specify the *minimum* `@tauri-apps/api` version instead of a single exact version. +- [`6de87966`](https://github.com/tauri-apps/plugins-workspace/commit/6de87966ecc00ad9d91c25be452f1f46bd2b7e1f) ([#1597](https://github.com/tauri-apps/plugins-workspace/pull/1597) by [@Legend-Master](https://github.com/tauri-apps/plugins-workspace/../../Legend-Master)) Update to tauri beta.25. + +## \[2.0.0-beta.7] + +- [`22a17980`](https://github.com/tauri-apps/plugins-workspace/commit/22a17980ff4f6f8c40adb1b8f4ffc6dae2fe7e30) ([#1537](https://github.com/tauri-apps/plugins-workspace/pull/1537) by [@lucasfernog](https://github.com/tauri-apps/plugins-workspace/../../lucasfernog)) Update to tauri beta.24. + +## \[2.0.0-beta.6] + +- [`76daee7a`](https://github.com/tauri-apps/plugins-workspace/commit/76daee7aafece34de3092c86e531cf9eb1138989) ([#1512](https://github.com/tauri-apps/plugins-workspace/pull/1512) by [@renovate](https://github.com/tauri-apps/plugins-workspace/../../renovate)) Update to tauri beta.23. + +## \[2.0.0-beta.5] + +- [`9013854f`](https://github.com/tauri-apps/plugins-workspace/commit/9013854f42a49a230b9dbb9d02774765528a923f)([#1382](https://github.com/tauri-apps/plugins-workspace/pull/1382)) Update to tauri beta.22. + +## \[2.0.0-beta.4] + +- [`430bd6f4`](https://github.com/tauri-apps/plugins-workspace/commit/430bd6f4f379bee5d232ae6b098ae131db7f178a)([#1363](https://github.com/tauri-apps/plugins-workspace/pull/1363)) Update to tauri beta.20. + +## \[2.0.0-beta.5] + +- [`d9de5b19`](https://github.com/tauri-apps/plugins-workspace/commit/d9de5b19d1e950c06f0915ae92a862acb266d108)([#1283](https://github.com/tauri-apps/plugins-workspace/pull/1283)) Implement `WindowExt` for `WebviewWindow`. + +## \[2.0.0-beta.3] + +- [`bd1ed590`](https://github.com/tauri-apps/plugins-workspace/commit/bd1ed5903ffcce5500310dac1e59e8c67674ef1e)([#1237](https://github.com/tauri-apps/plugins-workspace/pull/1237)) Update to tauri beta.17. + +## \[2.0.0-beta.3] + +- [`a04ea2f`](https://github.com/tauri-apps/plugins-workspace/commit/a04ea2f38294d5a3987578283badc8eec87a7752)([#1071](https://github.com/tauri-apps/plugins-workspace/pull/1071)) The global API script is now only added to the binary when the `withGlobalTauri` config is true. + +## \[2.0.0-beta.2] + +- [`99bea25`](https://github.com/tauri-apps/plugins-workspace/commit/99bea2559c2c0648c2519c50a18cd124dacef57b)([#1005](https://github.com/tauri-apps/plugins-workspace/pull/1005)) Update to tauri beta.8. + +## \[2.0.0-beta.1] + +- [`569defb`](https://github.com/tauri-apps/plugins-workspace/commit/569defbe9492e38938554bb7bdc1be9151456d21) Update to tauri beta.4. + +## \[2.0.0-beta.0] + +- [`d198c01`](https://github.com/tauri-apps/plugins-workspace/commit/d198c014863ee260cb0de88a14b7fc4356ef7474)([#862](https://github.com/tauri-apps/plugins-workspace/pull/862)) Update to tauri beta. + +## \[2.0.0-alpha.5] + +- [`387c2f9`](https://github.com/tauri-apps/plugins-workspace/commit/387c2f9e0ce4c75c07ffa3fd76391a25b58f5daf)([#802](https://github.com/tauri-apps/plugins-workspace/pull/802)) Update to @tauri-apps/api v2.0.0-alpha.13. + +## \[2.0.0-alpha.4] + +- [`387c2f9`](https://github.com/tauri-apps/plugins-workspace/commit/387c2f9e0ce4c75c07ffa3fd76391a25b58f5daf)([#802](https://github.com/tauri-apps/plugins-workspace/pull/802)) Update to @tauri-apps/api v2.0.0-alpha.12. + +## \[2.0.0-alpha.3] + +- [`e438e0a`](https://github.com/tauri-apps/plugins-workspace/commit/e438e0a62d4b430a5159f05f13ecd397dd891a0d)([#676](https://github.com/tauri-apps/plugins-workspace/pull/676)) Update to @tauri-apps/api v2.0.0-alpha.11. + +## \[2.0.0-alpha.2] + +- [`5c13736`](https://github.com/tauri-apps/plugins-workspace/commit/5c137365c60790e8d4037d449e8237aa3fffdab0)([#673](https://github.com/tauri-apps/plugins-workspace/pull/673)) Update to @tauri-apps/api v2.0.0-alpha.9. + +## \[2.0.0-alpha.2] + +- [`4e2cef9`](https://github.com/tauri-apps/plugins-workspace/commit/4e2cef9b702bbbb9cf4ee17de50791cb21f1b2a4)([#593](https://github.com/tauri-apps/plugins-workspace/pull/593)) Update to alpha.12. + +## \[2.0.0-alpha.1] + +- [`d74fc0a`](https://github.com/tauri-apps/plugins-workspace/commit/d74fc0a097996e90a37be8f57d50b7d1f6ca616f)([#555](https://github.com/tauri-apps/plugins-workspace/pull/555)) Update to alpha.11. + +## \[2.0.0-alpha.0] + +- [`717ae67`](https://github.com/tauri-apps/plugins-workspace/commit/717ae670978feb4492fac1f295998b93f2b9347f)([#371](https://github.com/tauri-apps/plugins-workspace/pull/371)) First v2 alpha release! + +## \[1.0.5] + +- `TrayLeft`, `TrayRight` and `TrayCenter` will now position the window according to the tray position relative to the monitor dimensions to prevent windows being displayed partially off-screen. + - [3d27909](https://github.com/tauri-apps/plugins-workspace/commit/3d279094d44be78cdc5d1de3938f1414e13db6b0) fix(positioner): Prevent tray relative windows from being moved off-screen ([#291](https://github.com/tauri-apps/plugins-workspace/pull/291)) on 2023-09-27 + +## \[0.2.7] + +- Update Tauri to v1.0.0 + - Bumped due to a bump in tauri-plugin-positioner. + - [0bb73eb](https://www.github.com/JonasKruckenberg/tauri-plugin-positioner/commit/0bb73eb20dae87f730c0b5f4cc08e6689e25fdba) Create tauri-v1.md on 2022-06-16 + +## \[0.2.6] + +- Update Tauri to v1.0.0-rc.12 + - Bumped due to a bump in tauri-plugin-positioner. + - [de6d76f](https://www.github.com/JonasKruckenberg/tauri-plugin-positioner/commit/de6d76f3a96a68e88a7ac434d65df4dbc82af122) Create update-tauri.md on 2022-05-25 + +## \[0.2.5] + +- Update deps + - Bumped due to a bump in tauri-plugin-positioner. + - [a8d3f73](https://www.github.com/JonasKruckenberg/tauri-plugin-positioner/commit/a8d3f73b74829ef5d53d4fb028e59d09e8946399) Create chore-update-deps.md on 2022-05-18 + +## \[0.2.4] + +- Update Tauri dependencies + - [2095b6a](https://www.github.com/JonasKruckenberg/tauri-plugin-positioner/commit/2095b6a4a4ab5590add099ddb2b1e8118e3496e4) add dep update changefile on 2022-02-14 + - [53d3a50](https://www.github.com/JonasKruckenberg/tauri-plugin-positioner/commit/53d3a501776f16741124aa961f521b9d7798c878) publish new versions ([#42](https://www.github.com/JonasKruckenberg/tauri-plugin-positioner/pull/42)) on 2022-02-14 + - [9f32726](https://www.github.com/JonasKruckenberg/tauri-plugin-positioner/commit/9f32726ede38bb9b2711f738a2f9fee7f0da2d73) Create update-deps.md on 2022-05-11 + +## \[0.2.3] + +- **Breaking Change**: Uses the new Tauri plugin builder pattern. Use `tauri_plugin_positioner::init()` instead of `tauri_plugin_positioner::Positioner::default()`. + - Bumped due to a bump in tauri-plugin-positioner. + - [14837a8](https://www.github.com/JonasKruckenberg/tauri-plugin-positioner/commit/14837a8d9cecdd6014867d4ef00fb98f21b2249d) refactor: use new builder pattern on 2022-02-26 + - [59874d8](https://www.github.com/JonasKruckenberg/tauri-plugin-positioner/commit/59874d827471dfb889662fadc74fec1f2243b89e) fix typo on 2022-02-26 + +## \[0.2.2] + +- Update README.md + - Bumped due to a bump in tauri-plugin-positioner. + - [92d6c3d](https://www.github.com/JonasKruckenberg/tauri-plugin-positioner/commit/92d6c3dca00a6b3562682804a649c0023831ce2b) Create docs-update-readme.md on 2022-02-17 + +## \[0.2.1] + +- Update `tauri` to `1.0.0-rc.1`, `serde` to `1.0.136` and `serde_json` to `1.0.79`. + - [2095b6a](https://www.github.com/JonasKruckenberg/tauri-plugin-positioner/commit/2095b6a4a4ab5590add099ddb2b1e8118e3496e4) add dep update changefile on 2022-02-14 + +## \[0.2.0] + +- Add SystemTray relative positions. + - [765b3ed](https://www.github.com/JonasKruckenberg/tauri-plugin-positioner/commit/765b3ed90056d85ae88b0852b7107ff2b84a6c3a) Create feat-tray-pos.md on 2022-01-19 + +## \[0.1.0] + +- Update package/crate metadata diff --git a/plugins/positioner/Cargo.toml b/plugins/positioner/Cargo.toml new file mode 100644 index 0000000..634eaa0 --- /dev/null +++ b/plugins/positioner/Cargo.toml @@ -0,0 +1,38 @@ +[package] +name = "tauri-plugin-positioner" +version = "2.3.4" +description = "Position your windows at well-known locations." +authors = { workspace = true } +license = { workspace = true } +edition = { workspace = true } +rust-version = { workspace = true } +repository = { workspace = true } +links = "tauri-plugin-positioner" +exclude = [ + "/banner.png", + "/guest-js", + "/package.json", + "/rollup.config.js", + "/tsconfig.json", +] + +[package.metadata.platforms.support] +windows = { level = "full", notes = "" } +linux = { level = "full", notes = "" } +macos = { level = "full", notes = "" } +android = { level = "none", notes = "" } +ios = { level = "none", notes = "" } + +[build-dependencies] +tauri-plugin = { workspace = true, features = ["build"] } + +[dependencies] +serde = { workspace = true } +serde_json = { workspace = true } +tauri = { workspace = true } +log = { workspace = true } +thiserror = { workspace = true } +serde_repr = "0.1" + +[features] +tray-icon = ["tauri/tray-icon"] diff --git a/plugins/positioner/LICENSE.spdx b/plugins/positioner/LICENSE.spdx new file mode 100644 index 0000000..cdd0df5 --- /dev/null +++ b/plugins/positioner/LICENSE.spdx @@ -0,0 +1,20 @@ +SPDXVersion: SPDX-2.1 +DataLicense: CC0-1.0 +PackageName: tauri +DataFormat: SPDXRef-1 +PackageSupplier: Organization: The Tauri Programme in the Commons Conservancy +PackageHomePage: https://tauri.app +PackageLicenseDeclared: Apache-2.0 +PackageLicenseDeclared: MIT +PackageCopyrightText: 2019-2022, The Tauri Programme in the Commons Conservancy +PackageSummary: Tauri is a rust project that enables developers to make secure +and small desktop applications using a web frontend. + +PackageComment: The package includes the following libraries; see +Relationship information. + +Created: 2019-05-20T09:00:00Z +PackageDownloadLocation: git://github.com/tauri-apps/tauri +PackageDownloadLocation: git+https://github.com/tauri-apps/tauri.git +PackageDownloadLocation: git+ssh://github.com/tauri-apps/tauri.git +Creator: Person: Daniel Thompson-Yvetot \ No newline at end of file diff --git a/plugins/positioner/LICENSE_APACHE-2.0 b/plugins/positioner/LICENSE_APACHE-2.0 new file mode 100644 index 0000000..4947287 --- /dev/null +++ b/plugins/positioner/LICENSE_APACHE-2.0 @@ -0,0 +1,177 @@ + + 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 \ No newline at end of file diff --git a/plugins/positioner/LICENSE_MIT b/plugins/positioner/LICENSE_MIT new file mode 100644 index 0000000..4d75472 --- /dev/null +++ b/plugins/positioner/LICENSE_MIT @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2017 - Present Tauri Apps 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. \ No newline at end of file diff --git a/plugins/positioner/README.md b/plugins/positioner/README.md new file mode 100644 index 0000000..8495b61 --- /dev/null +++ b/plugins/positioner/README.md @@ -0,0 +1,148 @@ +![plugin-positioner](https://github.com/tauri-apps/plugins-workspace/raw/v2/plugins/positioner/banner.png) + +Position your windows at well-known locations. + +This plugin is a port of [electron-positioner](https://github.com/jenslind/electron-positioner) for Tauri. + +| Platform | Supported | +| -------- | --------- | +| Linux | ✓ | +| Windows | ✓ | +| macOS | ✓ | +| Android | x | +| iOS | x | + +## Install + +_This plugin requires a Rust version of at least **1.77.2**_ + +There are three general methods of installation that we can recommend. + +1. Use crates.io and npm (easiest, and requires you to trust that our publishing pipeline worked) +2. Pull sources directly from Github using git tags / revision hashes (most secure) +3. Git submodule install this repo in your tauri project and then use file protocol to ingest the source (most secure, but inconvenient to use) + +Install the Core plugin by adding the following to your `Cargo.toml` file: + +`src-tauri/Cargo.toml` + +```toml +[dependencies] +tauri-plugin-positioner = "2.0.0" +# alternatively with Git: +tauri-plugin-positioner = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v2" } +``` + +You can install the JavaScript Guest bindings using your preferred JavaScript package manager: + +```sh +pnpm add @tauri-apps/plugin-positioner +# or +npm add @tauri-apps/plugin-positioner +# or +yarn add @tauri-apps/plugin-positioner +``` + +## Usage + +First you need to register the core plugin with Tauri: + +`src-tauri/src/lib.rs` + +```rust +use tauri::tray::TrayIconBuilder; + +fn main() { + tauri::Builder::default() + .plugin(tauri_plugin_positioner::init()) + // This is required to get tray-relative positions to work + .setup(|app| { + // note that this will create a new TrayIcon + TrayIconBuilder::new() + .on_tray_icon_event(|app, event| { + tauri_plugin_positioner::on_tray_event(app.app_handle(), &event); + }) + .build(app)?; + Ok(()) + }) + .run(tauri::generate_context!()) + .expect("error while running tauri application"); +} +``` + +Alternatively, you may handle the tray events through JavaScript. Register the plugin as previously noted. + +```rust +fn main() { + tauri::Builder::default() + .plugin(tauri_plugin_positioner::init()) + .run(tauri::generate_context!()) + .expect("error while running tauri application"); +} +``` + +And in JavaScript, the `action` passed to the TrayIcon should include the handler. + +```javascript +import { + moveWindow, + Position, + handleIconState, +} from "@tauri-apps/plugin-positioner"; + +const action = async (event: TrayIconEvent) => { + // add the handle in the action to update the state + await handleIconState(event); + + if (event.type === "Click") { + // note this option requires enabling the `tray-icon` + // feature in the Cargo.toml + await moveWindow(Position.TrayLeft); + } +}; + +const tray = await TrayIcon.new({ id: "main", action }); +``` + +Afterwards all the plugin's APIs are available through the JavaScript guest bindings: + +```javascript +import { moveWindow, Position } from '@tauri-apps/plugin-positioner' + +moveWindow(Position.TopRight) +``` + +If you only intend on moving the window from rust code, you can import the Window trait extension instead of registering the plugin: + +```rust +use tauri_plugin_positioner::{WindowExt, Position}; + +let mut win = app.get_window("main").unwrap(); +let _ = win.move_window(Position::TopRight); +``` + +## Contributing + +PRs accepted. Please make sure to read the Contributing Guide before making a pull request. + +## Partners + + + + + + + +
+ + CrabNebula + +
+ +For the complete list of sponsors please visit our [website](https://tauri.app#sponsors) and [Open Collective](https://opencollective.com/tauri). + +## License + +Code: (c) 2021 - Jonas Kruckenberg. 2021 - Present - The Tauri Programme within The Commons Conservancy. + +MIT or MIT/Apache 2.0 where applicable. diff --git a/plugins/positioner/SECURITY.md b/plugins/positioner/SECURITY.md new file mode 100644 index 0000000..4f09bba --- /dev/null +++ b/plugins/positioner/SECURITY.md @@ -0,0 +1,23 @@ +# Security Policy + +**Do not report security vulnerabilities through public GitHub issues.** + +**Please use the [Private Vulnerability Disclosure](https://docs.github.com/en/code-security/security-advisories/guidance-on-reporting-and-writing-information-about-vulnerabilities/privately-reporting-a-security-vulnerability#privately-reporting-a-security-vulnerability) feature of GitHub.** + +Include as much of the following information: + +- Type of issue (e.g. improper input parsing, privilege escalation, etc.) +- The location of the affected source code (tag/branch/commit or direct URL) +- Any special configuration required to reproduce the issue +- The distribution affected or used to help us with reproduction of the issue +- Step-by-step instructions to reproduce the issue +- Ideally a reproduction repository +- Impact of the issue, including how an attacker might exploit the issue + +We prefer to receive reports in English. + +## Contact + +Please disclose a vulnerability or security relevant issue here: [https://github.com/tauri-apps/plugins-workspace/security/advisories/new](https://github.com/tauri-apps/plugins-workspace/security/advisories/new). + +Alternatively, you can also contact us by email via [security@tauri.app](mailto:security@tauri.app). diff --git a/plugins/positioner/api-iife.js b/plugins/positioner/api-iife.js new file mode 100644 index 0000000..0163a51 --- /dev/null +++ b/plugins/positioner/api-iife.js @@ -0,0 +1 @@ +if("__TAURI__"in window){var __TAURI_PLUGIN_POSITIONER__=function(t,o){"use strict";var e;return t.Position=void 0,(e=t.Position||(t.Position={}))[e.TopLeft=0]="TopLeft",e[e.TopRight=1]="TopRight",e[e.BottomLeft=2]="BottomLeft",e[e.BottomRight=3]="BottomRight",e[e.TopCenter=4]="TopCenter",e[e.BottomCenter=5]="BottomCenter",e[e.LeftCenter=6]="LeftCenter",e[e.RightCenter=7]="RightCenter",e[e.Center=8]="Center",e[e.TrayLeft=9]="TrayLeft",e[e.TrayBottomLeft=10]="TrayBottomLeft",e[e.TrayRight=11]="TrayRight",e[e.TrayBottomRight=12]="TrayBottomRight",e[e.TrayCenter=13]="TrayCenter",e[e.TrayBottomCenter=14]="TrayBottomCenter",t.handleIconState=async function(t){await o.invoke("plugin:positioner|set_tray_icon_state",{position:t.rect.position,size:t.rect.size})},t.moveWindow=async function(t){await o.invoke("plugin:positioner|move_window",{position:t})},t.moveWindowConstrained=async function(t){await o.invoke("plugin:positioner|move_window_constrained",{position:t})},t}({},window.__TAURI__.core);Object.defineProperty(window.__TAURI__,"positioner",{value:__TAURI_PLUGIN_POSITIONER__})} diff --git a/plugins/positioner/banner.png b/plugins/positioner/banner.png new file mode 100644 index 0000000000000000000000000000000000000000..1461737ec46db9fcc2914f200d033ed0866c0834 GIT binary patch literal 43986 zcmX_ocR1Dm`~T}WIy!aGhlUfSYhVRbW)T$;`wlb=~VQ_h?{$PLle0sbV~AsyXt?u!%{{v!cPFA!~@OVA#I; zgQVyB6X(6fj}jrwTy>Mn4u+QE`uT>G+p8R;rxjldFkW|tw$`S*Y^GuN7T{dl)V#4w z!!?sCmu!!%QFnG|m;c~~W#?PU?Jn2d1c$mo=peeTzc*8(E6JqRv&>`u!^iV_zYFGN z3ry+{2PE=|N6G9OV|6RrTsv#QM_+M$4Dl~2zIw!Hwl_JvMIEiADu%4?ce3AVc(Lnc zzkh`Ic~Lz{+Cy7&su6il6G8abiPe4Kdl7!RO?tU@>&Jp≥6LIq;(Mh2|8i3m!>s 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 Hn8^PF&pwjJ literal 0 HcmV?d00001 diff --git a/plugins/positioner/build.rs b/plugins/positioner/build.rs new file mode 100644 index 0000000..830c61f --- /dev/null +++ b/plugins/positioner/build.rs @@ -0,0 +1,15 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +const COMMANDS: &[&str] = &[ + "move_window", + "move_window_constrained", + "set_tray_icon_state", +]; + +fn main() { + tauri_plugin::Builder::new(COMMANDS) + .global_api_script_path("./api-iife.js") + .build(); +} diff --git a/plugins/positioner/guest-js/index.ts b/plugins/positioner/guest-js/index.ts new file mode 100644 index 0000000..ba35480 --- /dev/null +++ b/plugins/positioner/guest-js/index.ts @@ -0,0 +1,140 @@ +// Copyright 2021 Jonas Kruckenberg +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +/** + * Move the window to well-known positions, including positions relative to the tray icon. + * + * @module + */ + +import { invoke } from '@tauri-apps/api/core' +import type { TrayIconEvent } from '@tauri-apps/api/tray' + +/** + * Well known window positions. + */ +export enum Position { + /** Top left corner of the screen. */ + TopLeft = 0, + /** Top right corner of the screen. */ + TopRight, + /** Bottom left corner of the screen. */ + BottomLeft, + /** Bottom right corner of the screen. */ + BottomRight, + /** Top center of the screen. */ + TopCenter, + /** Bottom center of the screen. */ + BottomCenter, + /** Vertically centered on the left edge of the screen. */ + LeftCenter, + /** Vertically centered on the right edge of the screen. */ + RightCenter, + /** Center of the screen. */ + Center, + /** + * Above the tray icon, aligned with its left edge. Requires the `tray-icon` feature and the + * tray icon's events to be forwarded through {@link handleIconState}. + */ + TrayLeft, + /** + * Directly below the tray icon, aligned with its left edge. Requires the `tray-icon` feature + * and the tray icon's events to be forwarded through {@link handleIconState}. + */ + TrayBottomLeft, + /** + * Above the tray icon, aligned with its right edge. Requires the `tray-icon` feature and the + * tray icon's events to be forwarded through {@link handleIconState}. + */ + TrayRight, + /** + * Directly below the tray icon, aligned with its right edge. Requires the `tray-icon` feature + * and the tray icon's events to be forwarded through {@link handleIconState}. + */ + TrayBottomRight, + /** + * Above the tray icon, horizontally centered on it. Requires the `tray-icon` feature and the + * tray icon's events to be forwarded through {@link handleIconState}. + */ + TrayCenter, + /** + * Directly below the tray icon, horizontally centered on it. Requires the `tray-icon` feature + * and the tray icon's events to be forwarded through {@link handleIconState}. + */ + TrayBottomCenter +} + +/** + * Moves the `Window` to the given {@link Position} using `WindowExt.move_window()` + * All positions are relative to the **current** screen. + * + * @example + * ```typescript + * import { moveWindow, Position } from '@tauri-apps/plugin-positioner' + * + * await moveWindow(Position.TopRight) + * ``` + * + * @param to The {@link Position} to move to. + * @since 2.0.0 + */ +export async function moveWindow(to: Position): Promise { + await invoke('plugin:positioner|move_window', { + position: to + }) +} + +/** + * Moves the `Window` to the given {@link Position} using `WindowExt.move_window_constrained()` + * + * This move operation constrains the window to the screen dimensions in case of + * tray-icon positions. + * + * @example + * ```typescript + * import { moveWindowConstrained, Position } from '@tauri-apps/plugin-positioner' + * + * await moveWindowConstrained(Position.TrayCenter) + * ``` + * + * @param to The (tray) {@link Position} to move to. + * @since 2.1.0 + */ +export async function moveWindowConstrained(to: Position): Promise { + await invoke('plugin:positioner|move_window_constrained', { + position: to + }) +} + +/** + * Reports the tray icon's current position and size so the `Tray*` {@link Position} variants + * can be resolved. + * + * This is an internal helper meant to be called from the tray icon's event handler (the + * `action` callback passed to `TrayIcon.new()`), forwarding every {@link TrayIconEvent} it + * receives so that {@link moveWindow} and {@link moveWindowConstrained} can later position the + * window relative to the tray icon. + * + * @example + * ```typescript + * import { handleIconState } from '@tauri-apps/plugin-positioner' + * import { TrayIcon, type TrayIconEvent } from '@tauri-apps/api/tray' + * + * const action = async (event: TrayIconEvent) => { + * await handleIconState(event) + * } + * + * const tray = await TrayIcon.new({ id: 'main', action }) + * ``` + * + * @param event The tray icon event to read the position and size from. + * @since 2.0.0 + */ +export async function handleIconState(event: TrayIconEvent): Promise { + await invoke('plugin:positioner|set_tray_icon_state', { + position: event.rect.position, + size: event.rect.size + }) +} diff --git a/plugins/positioner/package.json b/plugins/positioner/package.json new file mode 100644 index 0000000..bd27bba --- /dev/null +++ b/plugins/positioner/package.json @@ -0,0 +1,30 @@ +{ + "name": "@tauri-apps/plugin-positioner", + "version": "2.3.4", + "description": "Position your windows at well-known locations.", + "license": "MIT OR Apache-2.0", + "authors": [ + "Tauri Programme within The Commons Conservancy" + ], + "repository": "https://github.com/tauri-apps/plugins-workspace", + "type": "module", + "types": "./dist-js/index.d.ts", + "main": "./dist-js/index.cjs", + "module": "./dist-js/index.js", + "exports": { + "types": "./dist-js/index.d.ts", + "import": "./dist-js/index.js", + "require": "./dist-js/index.cjs" + }, + "scripts": { + "build": "rollup -c" + }, + "files": [ + "dist-js", + "README.md", + "LICENSE" + ], + "dependencies": { + "@tauri-apps/api": "^2.11.0" + } +} diff --git a/plugins/positioner/permissions/autogenerated/commands/move_window.toml b/plugins/positioner/permissions/autogenerated/commands/move_window.toml new file mode 100644 index 0000000..f78f9c7 --- /dev/null +++ b/plugins/positioner/permissions/autogenerated/commands/move_window.toml @@ -0,0 +1,13 @@ +# Automatically generated - DO NOT EDIT! + +"$schema" = "../../schemas/schema.json" + +[[permission]] +identifier = "allow-move-window" +description = "Enables the move_window command without any pre-configured scope." +commands.allow = ["move_window"] + +[[permission]] +identifier = "deny-move-window" +description = "Denies the move_window command without any pre-configured scope." +commands.deny = ["move_window"] diff --git a/plugins/positioner/permissions/autogenerated/commands/move_window_constrained.toml b/plugins/positioner/permissions/autogenerated/commands/move_window_constrained.toml new file mode 100644 index 0000000..8034399 --- /dev/null +++ b/plugins/positioner/permissions/autogenerated/commands/move_window_constrained.toml @@ -0,0 +1,13 @@ +# Automatically generated - DO NOT EDIT! + +"$schema" = "../../schemas/schema.json" + +[[permission]] +identifier = "allow-move-window-constrained" +description = "Enables the move_window_constrained command without any pre-configured scope." +commands.allow = ["move_window_constrained"] + +[[permission]] +identifier = "deny-move-window-constrained" +description = "Denies the move_window_constrained command without any pre-configured scope." +commands.deny = ["move_window_constrained"] diff --git a/plugins/positioner/permissions/autogenerated/commands/set_tray_icon_state.toml b/plugins/positioner/permissions/autogenerated/commands/set_tray_icon_state.toml new file mode 100644 index 0000000..6b85f63 --- /dev/null +++ b/plugins/positioner/permissions/autogenerated/commands/set_tray_icon_state.toml @@ -0,0 +1,13 @@ +# Automatically generated - DO NOT EDIT! + +"$schema" = "../../schemas/schema.json" + +[[permission]] +identifier = "allow-set-tray-icon-state" +description = "Enables the set_tray_icon_state command without any pre-configured scope." +commands.allow = ["set_tray_icon_state"] + +[[permission]] +identifier = "deny-set-tray-icon-state" +description = "Denies the set_tray_icon_state command without any pre-configured scope." +commands.deny = ["set_tray_icon_state"] diff --git a/plugins/positioner/permissions/autogenerated/reference.md b/plugins/positioner/permissions/autogenerated/reference.md new file mode 100644 index 0000000..f6e0913 --- /dev/null +++ b/plugins/positioner/permissions/autogenerated/reference.md @@ -0,0 +1,97 @@ +## Default Permission + +Allows the moveWindow and handleIconState APIs + +#### This default permission set includes the following: + +- `allow-move-window` +- `allow-move-window-constrained` +- `allow-set-tray-icon-state` + +## Permission Table + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
IdentifierDescription
+ +`positioner:allow-move-window` + + + +Enables the move_window command without any pre-configured scope. + +
+ +`positioner:deny-move-window` + + + +Denies the move_window command without any pre-configured scope. + +
+ +`positioner:allow-move-window-constrained` + + + +Enables the move_window_constrained command without any pre-configured scope. + +
+ +`positioner:deny-move-window-constrained` + + + +Denies the move_window_constrained command without any pre-configured scope. + +
+ +`positioner:allow-set-tray-icon-state` + + + +Enables the set_tray_icon_state command without any pre-configured scope. + +
+ +`positioner:deny-set-tray-icon-state` + + + +Denies the set_tray_icon_state command without any pre-configured scope. + +
diff --git a/plugins/positioner/permissions/default.toml b/plugins/positioner/permissions/default.toml new file mode 100644 index 0000000..7fc3c3d --- /dev/null +++ b/plugins/positioner/permissions/default.toml @@ -0,0 +1,8 @@ +"$schema" = "schemas/schema.json" +[default] +description = "Allows the moveWindow and handleIconState APIs" +permissions = [ + "allow-move-window", + "allow-move-window-constrained", + "allow-set-tray-icon-state", +] diff --git a/plugins/positioner/permissions/schemas/schema.json b/plugins/positioner/permissions/schemas/schema.json new file mode 100644 index 0000000..b0fc760 --- /dev/null +++ b/plugins/positioner/permissions/schemas/schema.json @@ -0,0 +1,342 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "PermissionFile", + "description": "Permission file that can define a default permission, a set of permissions or a list of inlined permissions.", + "type": "object", + "properties": { + "default": { + "description": "The default permission set for the plugin", + "anyOf": [ + { + "$ref": "#/definitions/DefaultPermission" + }, + { + "type": "null" + } + ] + }, + "set": { + "description": "A list of permissions sets defined", + "type": "array", + "items": { + "$ref": "#/definitions/PermissionSet" + } + }, + "permission": { + "description": "A list of inlined permissions", + "default": [], + "type": "array", + "items": { + "$ref": "#/definitions/Permission" + } + } + }, + "definitions": { + "DefaultPermission": { + "description": "The default permission set of the plugin.\n\nWorks similarly to a permission with the \"default\" identifier.", + "type": "object", + "required": [ + "permissions" + ], + "properties": { + "version": { + "description": "The version of the permission.", + "type": [ + "integer", + "null" + ], + "format": "uint64", + "minimum": 1.0 + }, + "description": { + "description": "Human-readable description of what the permission does. Tauri convention is to use `

` headings in markdown content for Tauri documentation generation purposes.", + "type": [ + "string", + "null" + ] + }, + "permissions": { + "description": "All permissions this set contains.", + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "PermissionSet": { + "description": "A set of direct permissions grouped together under a new name.", + "type": "object", + "required": [ + "description", + "identifier", + "permissions" + ], + "properties": { + "identifier": { + "description": "A unique identifier for the permission.", + "type": "string" + }, + "description": { + "description": "Human-readable description of what the permission does.", + "type": "string" + }, + "permissions": { + "description": "All permissions this set contains.", + "type": "array", + "items": { + "$ref": "#/definitions/PermissionKind" + } + } + } + }, + "Permission": { + "description": "Descriptions of explicit privileges of commands.\n\nIt can enable commands to be accessible in the frontend of the application.\n\nIf the scope is defined it can be used to fine grain control the access of individual or multiple commands.", + "type": "object", + "required": [ + "identifier" + ], + "properties": { + "version": { + "description": "The version of the permission.", + "type": [ + "integer", + "null" + ], + "format": "uint64", + "minimum": 1.0 + }, + "identifier": { + "description": "A unique identifier for the permission.", + "type": "string" + }, + "description": { + "description": "Human-readable description of what the permission does. Tauri internal convention is to use `

` headings in markdown content for Tauri documentation generation purposes.", + "type": [ + "string", + "null" + ] + }, + "commands": { + "description": "Allowed or denied commands when using this permission.", + "default": { + "allow": [], + "deny": [] + }, + "allOf": [ + { + "$ref": "#/definitions/Commands" + } + ] + }, + "scope": { + "description": "Allowed or denied scoped when using this permission.", + "allOf": [ + { + "$ref": "#/definitions/Scopes" + } + ] + }, + "platforms": { + "description": "Target platforms this permission applies. By default all platforms are affected by this permission.", + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/Target" + } + } + } + }, + "Commands": { + "description": "Allowed and denied commands inside a permission.\n\nIf two commands clash inside of `allow` and `deny`, it should be denied by default.", + "type": "object", + "properties": { + "allow": { + "description": "Allowed command.", + "default": [], + "type": "array", + "items": { + "type": "string" + } + }, + "deny": { + "description": "Denied command, which takes priority.", + "default": [], + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "Scopes": { + "description": "An argument for fine grained behavior control of Tauri commands.\n\nIt can be of any serde serializable type and is used to allow or prevent certain actions inside a Tauri command. The configured scope is passed to the command and will be enforced by the command implementation.\n\n## Example\n\n```json { \"allow\": [{ \"path\": \"$HOME/**\" }], \"deny\": [{ \"path\": \"$HOME/secret.txt\" }] } ```", + "type": "object", + "properties": { + "allow": { + "description": "Data that defines what is allowed by the scope.", + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/Value" + } + }, + "deny": { + "description": "Data that defines what is denied by the scope. This should be prioritized by validation logic.", + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/Value" + } + } + } + }, + "Value": { + "description": "All supported ACL values.", + "anyOf": [ + { + "description": "Represents a null JSON value.", + "type": "null" + }, + { + "description": "Represents a [`bool`].", + "type": "boolean" + }, + { + "description": "Represents a valid ACL [`Number`].", + "allOf": [ + { + "$ref": "#/definitions/Number" + } + ] + }, + { + "description": "Represents a [`String`].", + "type": "string" + }, + { + "description": "Represents a list of other [`Value`]s.", + "type": "array", + "items": { + "$ref": "#/definitions/Value" + } + }, + { + "description": "Represents a map of [`String`] keys to [`Value`]s.", + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/Value" + } + } + ] + }, + "Number": { + "description": "A valid ACL number.", + "anyOf": [ + { + "description": "Represents an [`i64`].", + "type": "integer", + "format": "int64" + }, + { + "description": "Represents a [`f64`].", + "type": "number", + "format": "double" + } + ] + }, + "Target": { + "description": "Platform target.", + "oneOf": [ + { + "description": "MacOS.", + "type": "string", + "enum": [ + "macOS" + ] + }, + { + "description": "Windows.", + "type": "string", + "enum": [ + "windows" + ] + }, + { + "description": "Linux.", + "type": "string", + "enum": [ + "linux" + ] + }, + { + "description": "Android.", + "type": "string", + "enum": [ + "android" + ] + }, + { + "description": "iOS.", + "type": "string", + "enum": [ + "iOS" + ] + } + ] + }, + "PermissionKind": { + "type": "string", + "oneOf": [ + { + "description": "Enables the move_window command without any pre-configured scope.", + "type": "string", + "const": "allow-move-window", + "markdownDescription": "Enables the move_window command without any pre-configured scope." + }, + { + "description": "Denies the move_window command without any pre-configured scope.", + "type": "string", + "const": "deny-move-window", + "markdownDescription": "Denies the move_window command without any pre-configured scope." + }, + { + "description": "Enables the move_window_constrained command without any pre-configured scope.", + "type": "string", + "const": "allow-move-window-constrained", + "markdownDescription": "Enables the move_window_constrained command without any pre-configured scope." + }, + { + "description": "Denies the move_window_constrained command without any pre-configured scope.", + "type": "string", + "const": "deny-move-window-constrained", + "markdownDescription": "Denies the move_window_constrained command without any pre-configured scope." + }, + { + "description": "Enables the set_tray_icon_state command without any pre-configured scope.", + "type": "string", + "const": "allow-set-tray-icon-state", + "markdownDescription": "Enables the set_tray_icon_state command without any pre-configured scope." + }, + { + "description": "Denies the set_tray_icon_state command without any pre-configured scope.", + "type": "string", + "const": "deny-set-tray-icon-state", + "markdownDescription": "Denies the set_tray_icon_state command without any pre-configured scope." + }, + { + "description": "Allows the moveWindow and handleIconState APIs\n#### This default permission set includes:\n\n- `allow-move-window`\n- `allow-move-window-constrained`\n- `allow-set-tray-icon-state`", + "type": "string", + "const": "default", + "markdownDescription": "Allows the moveWindow and handleIconState APIs\n#### This default permission set includes:\n\n- `allow-move-window`\n- `allow-move-window-constrained`\n- `allow-set-tray-icon-state`" + } + ] + } + } +} \ No newline at end of file diff --git a/plugins/positioner/rollup.config.js b/plugins/positioner/rollup.config.js new file mode 100644 index 0000000..1f349ec --- /dev/null +++ b/plugins/positioner/rollup.config.js @@ -0,0 +1,7 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +import { createConfig } from '../../shared/rollup.config.js' + +export default createConfig() diff --git a/plugins/positioner/src/ext.rs b/plugins/positioner/src/ext.rs new file mode 100644 index 0000000..a27c18a --- /dev/null +++ b/plugins/positioner/src/ext.rs @@ -0,0 +1,346 @@ +// Copyright 2021 Jonas Kruckenberg +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +#[cfg(feature = "tray-icon")] +use crate::Tray; +use serde_repr::Deserialize_repr; +#[cfg(feature = "tray-icon")] +use tauri::Manager; +#[cfg(feature = "tray-icon")] +use tauri::Monitor; +use tauri::{PhysicalPosition, PhysicalSize, Result, Runtime, WebviewWindow, Window}; + +/// Well known window positions. +/// +/// The `Tray*` variants require the `tray-icon` feature and only resolve once the tray icon has +/// reported its position (see `on_tray_event`); using one before that happens returns an error. +#[derive(Debug, Deserialize_repr)] +#[repr(u16)] +pub enum Position { + /// Top left corner of the current screen. + TopLeft = 0, + /// Top right corner of the current screen. + TopRight, + /// Bottom left corner of the current screen. + BottomLeft, + /// Bottom right corner of the current screen. + BottomRight, + /// Top center of the current screen. + TopCenter, + /// Bottom center of the current screen. + BottomCenter, + /// Vertically centered on the left edge of the current screen. + LeftCenter, + /// Vertically centered on the right edge of the current screen. + RightCenter, + /// Center of the current screen. + Center, + /// Above the tray icon, aligning the window's left edge with the tray icon's left edge. On + /// Windows and macOS the window moves below the icon instead when there is not enough room + /// above. + #[cfg(feature = "tray-icon")] + TrayLeft, + /// Directly below the tray icon, aligning the window's left edge with the tray icon's left + /// edge. + #[cfg(feature = "tray-icon")] + TrayBottomLeft, + /// Above the tray icon, aligning the window's left edge with the tray icon's right edge. On + /// Windows and macOS the window moves below the icon instead when there is not enough room + /// above. + #[cfg(feature = "tray-icon")] + TrayRight, + /// Directly below the tray icon, aligning the window's left edge with the tray icon's right + /// edge. + #[cfg(feature = "tray-icon")] + TrayBottomRight, + /// Above the tray icon, horizontally centered on it. On Windows and macOS the window moves + /// below the icon instead when there is not enough room above. + #[cfg(feature = "tray-icon")] + TrayCenter, + /// Directly below the tray icon, horizontally centered on it. + #[cfg(feature = "tray-icon")] + TrayBottomCenter, +} + +/// A [`Window`] extension that provides extra methods related to positioning. +pub trait WindowExt { + /// Moves the [`Window`] to the given [`Position`] + /// + /// All (non-tray) positions are relative to the **current** screen. + fn move_window(&self, position: Position) -> Result<()>; + #[cfg(feature = "tray-icon")] + /// Moves the [`Window`] to the given [`Position`] while constraining Tray Positions to the dimensions of the screen. + /// + /// All non-tray positions will not be constrained by this method. + /// + /// This method allows you to position your Tray Windows without having them + /// cut off on the screen borders. + fn move_window_constrained(&self, position: Position) -> Result<()>; +} + +impl WindowExt for WebviewWindow { + fn move_window(&self, pos: Position) -> Result<()> { + self.as_ref().window().move_window(pos) + } + + #[cfg(feature = "tray-icon")] + fn move_window_constrained(&self, position: Position) -> Result<()> { + self.as_ref().window().move_window_constrained(position) + } +} + +impl WindowExt for Window { + #[cfg(feature = "tray-icon")] + fn move_window_constrained(&self, position: Position) -> Result<()> { + // Diverge to basic move_window, if the position is not a tray position + if !matches!( + position, + Position::TrayLeft + | Position::TrayBottomLeft + | Position::TrayRight + | Position::TrayBottomRight + | Position::TrayCenter + | Position::TrayBottomCenter + ) { + return self.move_window(position); + } + + let window_position = calculate_position(self, position)?; + let monitor = get_monitor_for_tray_icon(self)?; + if let Some(monitor) = monitor { + let monitor_size = monitor.size(); + let monitor_position = monitor.position(); + let window_size = self.outer_size()?; + + let right_border_monitor = monitor_position.x as f64 + monitor_size.width as f64; + let left_border_monitor = monitor_position.x as f64; + let right_border_window = window_position.x as f64 + window_size.width as f64; + let left_border_window = window_position.x as f64; + + let constrained_x = if left_border_window < left_border_monitor { + left_border_monitor + } else if right_border_window > right_border_monitor { + right_border_monitor - window_size.width as f64 + } else { + window_position.x as f64 + }; + + let bottom_border_monitor = monitor_position.y as f64 + monitor_size.height as f64; + let top_border_monitor = monitor_position.y as f64; + let bottom_border_window = window_position.y as f64 + window_size.height as f64; + let top_border_window = window_position.y as f64; + + let constrained_y = if top_border_window < top_border_monitor { + top_border_monitor + } else if bottom_border_window > bottom_border_monitor { + bottom_border_monitor - window_size.height as f64 + } else { + window_position.y as f64 + }; + + self.set_position(PhysicalPosition::new(constrained_x, constrained_y))?; + } else { + // Fallback on non constrained positioning + self.set_position(window_position)?; + } + + Ok(()) + } + + fn move_window(&self, pos: Position) -> Result<()> { + let position = calculate_position(self, pos)?; + self.set_position(position) + } +} + +#[cfg(feature = "tray-icon")] +/// Retrieve the monitor, where the tray icon is located on. +fn get_monitor_for_tray_icon(window: &Window) -> Result> { + let tray_position = window + .state::() + .0 + .lock() + .unwrap() + .map(|(pos, _)| pos) + .unwrap_or_default(); + + window.monitor_from_point(tray_position.x, tray_position.y) +} + +/// Calculate the top-left position of the window based on the given +/// [`Position`]. +fn calculate_position( + window: &Window, + pos: Position, +) -> Result> { + use Position::*; + + let screen = window.current_monitor()?.ok_or_else(|| { + tauri::Error::Io(std::io::Error::other("No monitor found for the window")) + })?; + // Only use the screen_position for the Tray independent positioning, + // because a tray event may not be called on the currently active monitor. + let screen_position = screen.position(); + let screen_size = PhysicalSize:: { + width: screen.size().width as i32, + height: screen.size().height as i32, + }; + let window_size = PhysicalSize:: { + width: window.outer_size()?.width as i32, + height: window.outer_size()?.height as i32, + }; + #[cfg(feature = "tray-icon")] + let (tray_position, tray_size) = window + .state::() + .0 + .lock() + .unwrap() + .map(|(pos, size)| { + ( + Some((pos.x as i32, pos.y as i32)), + Some((size.width as i32, size.height as i32)), + ) + }) + .unwrap_or_default(); + + let physical_pos = match pos { + TopLeft => *screen_position, + TopRight => PhysicalPosition { + x: screen_position.x + (screen_size.width - window_size.width), + y: screen_position.y, + }, + BottomLeft => PhysicalPosition { + x: screen_position.x, + y: screen_size.height - (window_size.height - screen_position.y), + }, + BottomRight => PhysicalPosition { + x: screen_position.x + (screen_size.width - window_size.width), + y: screen_size.height - (window_size.height - screen_position.y), + }, + TopCenter => PhysicalPosition { + x: screen_position.x + ((screen_size.width / 2) - (window_size.width / 2)), + y: screen_position.y, + }, + BottomCenter => PhysicalPosition { + x: screen_position.x + ((screen_size.width / 2) - (window_size.width / 2)), + y: screen_size.height - (window_size.height - screen_position.y), + }, + LeftCenter => PhysicalPosition { + x: screen_position.x, + y: screen_position.y + (screen_size.height / 2) - (window_size.height / 2), + }, + RightCenter => PhysicalPosition { + x: screen_position.x + (screen_size.width - window_size.width), + y: screen_position.y + (screen_size.height / 2) - (window_size.height / 2), + }, + Center => PhysicalPosition { + x: screen_position.x + ((screen_size.width / 2) - (window_size.width / 2)), + y: screen_position.y + (screen_size.height / 2) - (window_size.height / 2), + }, + #[cfg(feature = "tray-icon")] + TrayLeft => { + if let (Some((tray_x, tray_y)), Some((_, _tray_height))) = (tray_position, tray_size) { + let y = tray_y - window_size.height; + // Choose y value based on the target OS + #[cfg(target_os = "windows")] + let y = if y < 0 { tray_y + _tray_height } else { y }; + + #[cfg(target_os = "macos")] + let y = if y < 0 { tray_y } else { y }; + + PhysicalPosition { x: tray_x, y } + } else { + return Err(tauri::Error::Io(std::io::Error::other( + "Tray position not set", + ))); + } + } + #[cfg(feature = "tray-icon")] + TrayBottomLeft => { + if let Some((tray_x, tray_y)) = tray_position { + PhysicalPosition { + x: tray_x, + y: tray_y, + } + } else { + return Err(tauri::Error::Io(std::io::Error::other( + "Tray position not set", + ))); + } + } + #[cfg(feature = "tray-icon")] + TrayRight => { + if let (Some((tray_x, tray_y)), Some((tray_width, _tray_height))) = + (tray_position, tray_size) + { + let y = tray_y - window_size.height; + // Choose y value based on the target OS + #[cfg(target_os = "windows")] + let y = if y < 0 { tray_y + _tray_height } else { y }; + + #[cfg(target_os = "macos")] + let y = if y < 0 { tray_y } else { y }; + + PhysicalPosition { + x: tray_x + tray_width, + y, + } + } else { + return Err(tauri::Error::Io(std::io::Error::other( + "Tray position not set", + ))); + } + } + #[cfg(feature = "tray-icon")] + TrayBottomRight => { + if let (Some((tray_x, tray_y)), Some((tray_width, _))) = (tray_position, tray_size) { + PhysicalPosition { + x: tray_x + tray_width, + y: tray_y, + } + } else { + return Err(tauri::Error::Io(std::io::Error::other( + "Tray position not set", + ))); + } + } + #[cfg(feature = "tray-icon")] + TrayCenter => { + if let (Some((tray_x, tray_y)), Some((tray_width, _tray_height))) = + (tray_position, tray_size) + { + let x = tray_x + tray_width / 2 - window_size.width / 2; + let y = tray_y - window_size.height; + // Choose y value based on the target OS + #[cfg(target_os = "windows")] + let y = if y < 0 { tray_y + _tray_height } else { y }; + + #[cfg(target_os = "macos")] + let y = if y < 0 { tray_y } else { y }; + + PhysicalPosition { x, y } + } else { + return Err(tauri::Error::Io(std::io::Error::other( + "Tray position not set", + ))); + } + } + #[cfg(feature = "tray-icon")] + TrayBottomCenter => { + if let (Some((tray_x, tray_y)), Some((tray_width, _))) = (tray_position, tray_size) { + PhysicalPosition { + x: tray_x + (tray_width / 2) - (window_size.width / 2), + y: tray_y, + } + } else { + return Err(tauri::Error::Io(std::io::Error::other( + "Tray position not set", + ))); + } + } + }; + + Ok(physical_pos) +} diff --git a/plugins/positioner/src/lib.rs b/plugins/positioner/src/lib.rs new file mode 100644 index 0000000..fd5e826 --- /dev/null +++ b/plugins/positioner/src/lib.rs @@ -0,0 +1,114 @@ +// Copyright 2021 Jonas Kruckenberg +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +//! A plugin for Tauri that helps position your windows at well-known locations. +//! +//! ## Cargo features +//! +//! - **tray-icon**: Enables tray-icon-relative positions. +//! +//! Note: This requires attaching the Tauri plugin, *even* when using the trait extension only. + +#![doc( + html_logo_url = "https://github.com/tauri-apps/tauri/raw/dev/app-icon.png", + html_favicon_url = "https://github.com/tauri-apps/tauri/raw/dev/app-icon.png" +)] +#![cfg(not(any(target_os = "android", target_os = "ios")))] + +mod ext; + +pub use ext::*; +use tauri::{ + plugin::{self, TauriPlugin}, + Result, Runtime, +}; + +#[cfg(feature = "tray-icon")] +use tauri::{tray::TrayIconEvent, AppHandle, Manager, PhysicalPosition, PhysicalSize}; + +#[cfg(feature = "tray-icon")] +struct Tray(std::sync::Mutex, PhysicalSize)>>); + +/// Records the tray icon's latest position and size so that the `Tray*` [`Position`] variants +/// (e.g. [`Position::TrayLeft`]) can be resolved by [`WindowExt::move_window`] and +/// [`WindowExt::move_window_constrained`]. +/// +/// Call this from your tray icon's event handler. Only [`TrayIconEvent::Click`], +/// [`TrayIconEvent::Enter`], [`TrayIconEvent::Leave`] and [`TrayIconEvent::Move`] carry the +/// icon's position and update the tracked value; other events are ignored. Until this has been +/// called at least once, moving a window to a `Tray*` position fails. +/// +/// Requires the `tray-icon` feature. +#[cfg(feature = "tray-icon")] +pub fn on_tray_event(app: &AppHandle, event: &TrayIconEvent) { + let (position, size) = { + match event { + TrayIconEvent::Click { rect, .. } + | TrayIconEvent::Enter { rect, .. } + | TrayIconEvent::Leave { rect, .. } + | TrayIconEvent::Move { rect, .. } => { + // tray-icon emits PhysicalSize so the scale factor should not matter. + let size = rect.size.to_physical(1.0); + let position = rect.position.to_physical(1.0); + (position, size) + } + + _ => return, + } + }; + + app.state::() + .0 + .lock() + .unwrap() + .replace((position, size)); +} + +#[tauri::command] +async fn move_window(window: tauri::Window, position: Position) -> Result<()> { + window.move_window(position) +} + +#[cfg(feature = "tray-icon")] +#[tauri::command] +async fn move_window_constrained( + window: tauri::Window, + position: Position, +) -> Result<()> { + window.move_window_constrained(position) +} + +#[cfg(feature = "tray-icon")] +#[tauri::command] +fn set_tray_icon_state( + app: AppHandle, + position: PhysicalPosition, + size: PhysicalSize, +) { + app.state::() + .0 + .lock() + .unwrap() + .replace((position, size)); +} + +/// The Tauri plugin that exposes [`WindowExt::move_window`] to the webview. +pub fn init() -> TauriPlugin { + let plugin = plugin::Builder::new("positioner").invoke_handler(tauri::generate_handler![ + move_window, + #[cfg(feature = "tray-icon")] + move_window_constrained, + #[cfg(feature = "tray-icon")] + set_tray_icon_state + ]); + + #[cfg(feature = "tray-icon")] + let plugin = plugin.setup(|app_handle, _api| { + app_handle.manage(Tray(std::sync::Mutex::new(None))); + Ok(()) + }); + + plugin.build() +} diff --git a/plugins/positioner/tsconfig.json b/plugins/positioner/tsconfig.json new file mode 100644 index 0000000..4374468 --- /dev/null +++ b/plugins/positioner/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "rootDir": "guest-js" + }, + "include": ["guest-js/*.ts"] +} diff --git a/plugins/process/CHANGELOG.md b/plugins/process/CHANGELOG.md new file mode 100644 index 0000000..b5f88a6 --- /dev/null +++ b/plugins/process/CHANGELOG.md @@ -0,0 +1,106 @@ +# Changelog + +## \[2.3.1] + +- [`93426f85`](https://github.com/tauri-apps/plugins-workspace/commit/93426f85120f49beb9f40222bff45185a32d54a9) Fixed an issue that caused docs.rs builds to fail. No user facing changes. + +## \[2.3.0] + +- [`f209b2f2`](https://github.com/tauri-apps/plugins-workspace/commit/f209b2f23cb29133c97ad5961fb46ef794dbe063) ([#2804](https://github.com/tauri-apps/plugins-workspace/pull/2804) by [@renovate](https://github.com/tauri-apps/plugins-workspace/../../renovate)) Updated tauri to 2.6 + +## \[2.2.2] + +- [`f634e524`](https://github.com/tauri-apps/plugins-workspace/commit/f634e5248ebe428f8305a59f74c13fc15147fb8e) This is an "empty" release to update the plugins' source files on crates.io and docs.rs. This should fix docs.rs build failures for projects using tauri plugins as dependencies. + +## \[2.2.1] + +- [`d2aef2fd`](https://github.com/tauri-apps/plugins-workspace/commit/d2aef2fddbdfad6526935c55ac10a94171a4f5f5) ([#2581](https://github.com/tauri-apps/plugins-workspace/pull/2581)) Migrate restart to use tauri's new `AppHandle::request_restart` method + +## \[2.2.0] + +- [`3a79266b`](https://github.com/tauri-apps/plugins-workspace/commit/3a79266b8cf96a55b1ae6339d725567d45a44b1d) ([#2173](https://github.com/tauri-apps/plugins-workspace/pull/2173) by [@FabianLars](https://github.com/tauri-apps/plugins-workspace/../../FabianLars)) Bumped all plugins to `v2.2.0`. From now, the versions for the Rust and JavaScript packages of each plugin will be in sync with each other. + +## \[2.0.1] + +- [`a1a82208`](https://github.com/tauri-apps/plugins-workspace/commit/a1a82208ed4ab87f83310be0dc95428aec9ab241) ([#1873](https://github.com/tauri-apps/plugins-workspace/pull/1873) by [@lucasfernog](https://github.com/tauri-apps/plugins-workspace/../../lucasfernog)) Downgrade MSRV to 1.77.2 to support Windows 7. + +## \[2.0.0] + +- [`e2c4dfb6`](https://github.com/tauri-apps/plugins-workspace/commit/e2c4dfb6af43e5dd8d9ceba232c315f5febd55c1) Update to tauri v2 stable release. + +## \[2.0.0-rc.1] + +- [`e2e97db5`](https://github.com/tauri-apps/plugins-workspace/commit/e2e97db51983267f5be84d4f6f0278d58834d1f5) ([#1701](https://github.com/tauri-apps/plugins-workspace/pull/1701) by [@lucasfernog](https://github.com/tauri-apps/plugins-workspace/../../lucasfernog)) Update to tauri 2.0.0-rc.8 + +## \[2.0.0-rc.0] + +- [`9887d1`](https://github.com/tauri-apps/plugins-workspace/commit/9887d14bd0e971c4c0f5c1188fc4005d3fc2e29e) Update to tauri RC. + +## \[2.0.0-beta.8] + +- [`99d6ac0f`](https://github.com/tauri-apps/plugins-workspace/commit/99d6ac0f9506a6a4a1aa59c728157190a7441af6) ([#1606](https://github.com/tauri-apps/plugins-workspace/pull/1606) by [@FabianLars](https://github.com/tauri-apps/plugins-workspace/../../FabianLars)) The JS packages now specify the *minimum* `@tauri-apps/api` version instead of a single exact version. +- [`6de87966`](https://github.com/tauri-apps/plugins-workspace/commit/6de87966ecc00ad9d91c25be452f1f46bd2b7e1f) ([#1597](https://github.com/tauri-apps/plugins-workspace/pull/1597) by [@Legend-Master](https://github.com/tauri-apps/plugins-workspace/../../Legend-Master)) Update to tauri beta.25. + +## \[2.0.0-beta.7] + +- [`22a17980`](https://github.com/tauri-apps/plugins-workspace/commit/22a17980ff4f6f8c40adb1b8f4ffc6dae2fe7e30) ([#1537](https://github.com/tauri-apps/plugins-workspace/pull/1537) by [@lucasfernog](https://github.com/tauri-apps/plugins-workspace/../../lucasfernog)) Update to tauri beta.24. + +## \[2.0.0-beta.6] + +- [`76daee7a`](https://github.com/tauri-apps/plugins-workspace/commit/76daee7aafece34de3092c86e531cf9eb1138989) ([#1512](https://github.com/tauri-apps/plugins-workspace/pull/1512) by [@renovate](https://github.com/tauri-apps/plugins-workspace/../../renovate)) Update to tauri beta.23. + +## \[2.0.0-beta.5] + +- [`9013854f`](https://github.com/tauri-apps/plugins-workspace/commit/9013854f42a49a230b9dbb9d02774765528a923f)([#1382](https://github.com/tauri-apps/plugins-workspace/pull/1382)) Update to tauri beta.22. + +## \[2.0.0-beta.4] + +- [`430bd6f4`](https://github.com/tauri-apps/plugins-workspace/commit/430bd6f4f379bee5d232ae6b098ae131db7f178a)([#1363](https://github.com/tauri-apps/plugins-workspace/pull/1363)) Update to tauri beta.20. + +## \[2.0.0-beta.3] + +- [`bd1ed590`](https://github.com/tauri-apps/plugins-workspace/commit/bd1ed5903ffcce5500310dac1e59e8c67674ef1e)([#1237](https://github.com/tauri-apps/plugins-workspace/pull/1237)) Update to tauri beta.17. + +## \[2.0.0-beta.3] + +- [`a04ea2f`](https://github.com/tauri-apps/plugins-workspace/commit/a04ea2f38294d5a3987578283badc8eec87a7752)([#1071](https://github.com/tauri-apps/plugins-workspace/pull/1071)) The global API script is now only added to the binary when the `withGlobalTauri` config is true. + +## \[2.0.0-beta.2] + +- [`99bea25`](https://github.com/tauri-apps/plugins-workspace/commit/99bea2559c2c0648c2519c50a18cd124dacef57b)([#1005](https://github.com/tauri-apps/plugins-workspace/pull/1005)) Update to tauri beta.8. + +## \[2.0.0-beta.1] + +- [`569defb`](https://github.com/tauri-apps/plugins-workspace/commit/569defbe9492e38938554bb7bdc1be9151456d21) Update to tauri beta.4. + +## \[2.0.0-beta.0] + +- [`d198c01`](https://github.com/tauri-apps/plugins-workspace/commit/d198c014863ee260cb0de88a14b7fc4356ef7474)([#862](https://github.com/tauri-apps/plugins-workspace/pull/862)) Update to tauri beta. + +## \[2.0.0-alpha.5] + +- [`387c2f9`](https://github.com/tauri-apps/plugins-workspace/commit/387c2f9e0ce4c75c07ffa3fd76391a25b58f5daf)([#802](https://github.com/tauri-apps/plugins-workspace/pull/802)) Update to @tauri-apps/api v2.0.0-alpha.13. + +## \[2.0.0-alpha.4] + +- [`387c2f9`](https://github.com/tauri-apps/plugins-workspace/commit/387c2f9e0ce4c75c07ffa3fd76391a25b58f5daf)([#802](https://github.com/tauri-apps/plugins-workspace/pull/802)) Update to @tauri-apps/api v2.0.0-alpha.12. + +## \[2.0.0-alpha.3] + +- [`e438e0a`](https://github.com/tauri-apps/plugins-workspace/commit/e438e0a62d4b430a5159f05f13ecd397dd891a0d)([#676](https://github.com/tauri-apps/plugins-workspace/pull/676)) Update to @tauri-apps/api v2.0.0-alpha.11. + +## \[2.0.0-alpha.2] + +- [`5c13736`](https://github.com/tauri-apps/plugins-workspace/commit/5c137365c60790e8d4037d449e8237aa3fffdab0)([#673](https://github.com/tauri-apps/plugins-workspace/pull/673)) Update to @tauri-apps/api v2.0.0-alpha.9. + +## \[2.0.0-alpha.2] + +- [`4e2cef9`](https://github.com/tauri-apps/plugins-workspace/commit/4e2cef9b702bbbb9cf4ee17de50791cb21f1b2a4)([#593](https://github.com/tauri-apps/plugins-workspace/pull/593)) Update to alpha.12. + +## \[2.0.0-alpha.1] + +- [`d74fc0a`](https://github.com/tauri-apps/plugins-workspace/commit/d74fc0a097996e90a37be8f57d50b7d1f6ca616f)([#555](https://github.com/tauri-apps/plugins-workspace/pull/555)) Update to alpha.11. + +## \[2.0.0-alpha.0] + +- [`717ae67`](https://github.com/tauri-apps/plugins-workspace/commit/717ae670978feb4492fac1f295998b93f2b9347f)([#371](https://github.com/tauri-apps/plugins-workspace/pull/371)) First v2 alpha release! diff --git a/plugins/process/Cargo.toml b/plugins/process/Cargo.toml new file mode 100644 index 0000000..c586cb2 --- /dev/null +++ b/plugins/process/Cargo.toml @@ -0,0 +1,30 @@ +[package] +name = "tauri-plugin-process" +version = "2.3.1" +description = "Access the current process of your Tauri application." +edition = { workspace = true } +authors = { workspace = true } +license = { workspace = true } +rust-version = { workspace = true } +repository = { workspace = true } +links = "tauri-plugin-process" +exclude = [ + "/banner.png", + "/guest-js", + "/package.json", + "/rollup.config.js", + "/tsconfig.json", +] + +[package.metadata.platforms.support] +windows = { level = "full", notes = "" } +linux = { level = "full", notes = "" } +macos = { level = "full", notes = "" } +android = { level = "none", notes = "" } +ios = { level = "none", notes = "" } + +[build-dependencies] +tauri-plugin = { workspace = true, features = ["build"] } + +[dependencies] +tauri = { workspace = true } diff --git a/plugins/process/LICENSE.spdx b/plugins/process/LICENSE.spdx new file mode 100644 index 0000000..cdd0df5 --- /dev/null +++ b/plugins/process/LICENSE.spdx @@ -0,0 +1,20 @@ +SPDXVersion: SPDX-2.1 +DataLicense: CC0-1.0 +PackageName: tauri +DataFormat: SPDXRef-1 +PackageSupplier: Organization: The Tauri Programme in the Commons Conservancy +PackageHomePage: https://tauri.app +PackageLicenseDeclared: Apache-2.0 +PackageLicenseDeclared: MIT +PackageCopyrightText: 2019-2022, The Tauri Programme in the Commons Conservancy +PackageSummary: Tauri is a rust project that enables developers to make secure +and small desktop applications using a web frontend. + +PackageComment: The package includes the following libraries; see +Relationship information. + +Created: 2019-05-20T09:00:00Z +PackageDownloadLocation: git://github.com/tauri-apps/tauri +PackageDownloadLocation: git+https://github.com/tauri-apps/tauri.git +PackageDownloadLocation: git+ssh://github.com/tauri-apps/tauri.git +Creator: Person: Daniel Thompson-Yvetot \ No newline at end of file diff --git a/plugins/process/LICENSE_APACHE-2.0 b/plugins/process/LICENSE_APACHE-2.0 new file mode 100644 index 0000000..4947287 --- /dev/null +++ b/plugins/process/LICENSE_APACHE-2.0 @@ -0,0 +1,177 @@ + + 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 \ No newline at end of file diff --git a/plugins/process/LICENSE_MIT b/plugins/process/LICENSE_MIT new file mode 100644 index 0000000..4d75472 --- /dev/null +++ b/plugins/process/LICENSE_MIT @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2017 - Present Tauri Apps 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. \ No newline at end of file diff --git a/plugins/process/README.md b/plugins/process/README.md new file mode 100644 index 0000000..13a1c4e --- /dev/null +++ b/plugins/process/README.md @@ -0,0 +1,93 @@ +![plugin-process](https://github.com/tauri-apps/plugins-workspace/raw/v2/plugins/process/banner.png) + +This plugin provides APIs to access the current process. To spawn child processes, see the [`shell`](https://github.com/tauri-apps/tauri-plugin-shell) plugin. + +| Platform | Supported | +| -------- | --------- | +| Linux | ✓ | +| Windows | ✓ | +| macOS | ✓ | +| Android | x | +| iOS | x | + +## Install + +_This plugin requires a Rust version of at least **1.77.2**_ + +There are three general methods of installation that we can recommend. + +1. Use crates.io and npm (easiest, and requires you to trust that our publishing pipeline worked) +2. Pull sources directly from Github using git tags / revision hashes (most secure) +3. Git submodule install this repo in your tauri project and then use file protocol to ingest the source (most secure, but inconvenient to use) + +Install the Core plugin by adding the following to your `Cargo.toml` file: + +`src-tauri/Cargo.toml` + +```toml +[dependencies] +tauri-plugin-process = "2.0.0" +# alternatively with Git: +tauri-plugin-process = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v2" } +``` + +You can install the JavaScript Guest bindings using your preferred JavaScript package manager: + +```sh +pnpm add @tauri-apps/plugin-process +# or +npm add @tauri-apps/plugin-process +# or +yarn add @tauri-apps/plugin-process +``` + +## Usage + +First you need to register the core plugin with Tauri: + +`src-tauri/src/lib.rs` + +```rust +fn main() { + tauri::Builder::default() + .plugin(tauri_plugin_process::init()) + .run(tauri::generate_context!()) + .expect("error while running tauri application"); +} +``` + +Afterwards all the plugin's APIs are available through the JavaScript guest bindings: + +```javascript +import { exit, relaunch } from '@tauri-apps/plugin-process' +// exit the app with the given status code +await exit(0) +// restart the app +await relaunch() +``` + +## Contributing + +PRs accepted. Please make sure to read the Contributing Guide before making a pull request. + +## Partners + + + + + + + +
+ + CrabNebula + +
+ +For the complete list of sponsors please visit our [website](https://tauri.app#sponsors) and [Open Collective](https://opencollective.com/tauri). + +## License + +Code: (c) 2015 - Present - The Tauri Programme within The Commons Conservancy. + +MIT or MIT/Apache 2.0 where applicable. diff --git a/plugins/process/SECURITY.md b/plugins/process/SECURITY.md new file mode 100644 index 0000000..4f09bba --- /dev/null +++ b/plugins/process/SECURITY.md @@ -0,0 +1,23 @@ +# Security Policy + +**Do not report security vulnerabilities through public GitHub issues.** + +**Please use the [Private Vulnerability Disclosure](https://docs.github.com/en/code-security/security-advisories/guidance-on-reporting-and-writing-information-about-vulnerabilities/privately-reporting-a-security-vulnerability#privately-reporting-a-security-vulnerability) feature of GitHub.** + +Include as much of the following information: + +- Type of issue (e.g. improper input parsing, privilege escalation, etc.) +- The location of the affected source code (tag/branch/commit or direct URL) +- Any special configuration required to reproduce the issue +- The distribution affected or used to help us with reproduction of the issue +- Step-by-step instructions to reproduce the issue +- Ideally a reproduction repository +- Impact of the issue, including how an attacker might exploit the issue + +We prefer to receive reports in English. + +## Contact + +Please disclose a vulnerability or security relevant issue here: [https://github.com/tauri-apps/plugins-workspace/security/advisories/new](https://github.com/tauri-apps/plugins-workspace/security/advisories/new). + +Alternatively, you can also contact us by email via [security@tauri.app](mailto:security@tauri.app). diff --git a/plugins/process/api-iife.js b/plugins/process/api-iife.js new file mode 100644 index 0000000..d24fd8b --- /dev/null +++ b/plugins/process/api-iife.js @@ -0,0 +1 @@ +if("__TAURI__"in window){var __TAURI_PLUGIN_PROCESS__=function(_,n){"use strict";return _.exit=async function(_=0){await n.invoke("plugin:process|exit",{code:_})},_.relaunch=async function(){await n.invoke("plugin:process|restart")},_}({},window.__TAURI__.core);Object.defineProperty(window.__TAURI__,"process",{value:__TAURI_PLUGIN_PROCESS__})} diff --git a/plugins/process/banner.png b/plugins/process/banner.png new file mode 100644 index 0000000000000000000000000000000000000000..7634d6e4bb88d70582170fa0bc9a0b5a24f204af GIT binary patch literal 60700 zcmX_n1yq#Z^S&aYsHlK+NW6e9-CdF|DeEEv%F-YqBGM@;(!$cMupkRk5=t&8A-RGy z5>hKjcl*11{^$GW9L^CA?!9;J%rnnCGdJw6o(9Dw#!F|;oS}dC#M@GtgV~YV`VQJ%aGT;yCwUn;9_t%M}Cm(w8%=w zEyR<^;KR_X(OI0~1gR&Y#edjjR>s(}gF`PP1cHJt2WQ7bL9a^1j^+PpXXwN}=c%kS z+`d2XZ&z;n`NbdJ_a3eKugViM-FR`=P>6(Btz3~z^ zIogxs)j+~bHbhZiTyC4YTs0;@pmV97ORRV{*YTW~;puVs3BuWQ9afEdjMdFQs&p`{ z*`H6V^@owT$f&5?qR$gDZ84#gMGwld$C6O$f00_e_q00iX?@ny)qRwNtO;;%SQNZ> z@7~aa$C$H`zMkHEU=q?qQCz&GkjR7N9B&A+rJ$hbW@2RID68H6^XE^!hPwLBM--yg z2$66B8()Rv4(3W=j<|O6{%6dLAsrPaJEU1mC3GwE1bi{CWQI22d#G!?TzCH3BpBv`Z?_T@l@87@AfBW{W_C6v({&ee%3=Gn2xfOO=j}OfbYc*?_!lGVZrkl^`(3u`? z`s}eh`UIT_x%aQFJ!QSXKutaod*&+Xn%B6`-=cjRnm1Twjb~QlHSXSZ)<@P$)+hF* zDt&Amf3$h{OW~OH-eMulf!fC?hN1MoaGIt34W`ClY`tO#!JT{*qArv$F6}t@5uX=d z{N-MgpusouwjPGlL~56$Vba4xr06RCb%Boi;U`Gfe%{M8BLUBZM7I7 z;SbOsZDex+8a~dG*#9f1KZ>ED*O0DF7Q$;p|dqo)_(1Y$*T zWv8VZi)x<)784T@5wY|0JBrcD-3SNY@8bLXdWu$dDq+OcFf<|{N1WzGO#-jcK@g^Y zH#we=r5LF4S{=170)P0L6vToy?q~cGhwZlN!nWK_v)4RnrANRogUbYa4y@#?H{U_q zms=$*L*G4U{_?)!_Lzd_jkw~L1TCBO@87>?0_SRrijF?v$lXfW{yP*n1Kjc9qdbQx zBoqiPQ9XYtL(*k}62uq}2$`2P{<@gamv4H5&tHBvorEq|5?|-VWtBaMDu^tq8g+{R!wU?mUr{=@}8VJU5$dhyZFAW82FNtt2+j_ z`948w?)B|l4Yl);*pqL^x|K|V0lKpHsPLo<5N=Y4{3a9A*wZ{0=e{J@x7IFoSNdV3 zTmG{dwL?2IQgR9(O-9l#;S|R_2T6WODy>Dy-i?vj60v%zRY^nB946DnOb6w^Jj9+1 z---VI{k`tN8~B z*nn@pqP;ja<>T;uc@A;-3C_8@IMl7D{!{7FZVeu7ES^V%gYCZr{HZ-e4E}0ucmh1$ zEkq#%OSl6(r`<-Q{>Jo6^Xb7yBCZiFx5=-=i*!0QIGLQh#xn{l$q<<43g%frq6Oyt#>@l^@n)VenHdGy#$3vt1NmC`Sw*#%P3Inr7* z)G3&W6mONpG~iOb%$;(UZw=5v><`nmki_PcMMy3ArGQDrEq>FguCfP6w1uD547(dL zxGvp$<-i1~XgThzmQ&$9!6R=Ikc;#__-I`2k&&7zGjp0mdS+*5`InE*jiny3Ffp0R zC@7qKjPJXAPs_JzlYl`9^pd%s9PRJ3t13nB_^xOr_7`g2YEEf5s_~$EmcG!*O*ggz zBRykCE!^}_XErTGqXBx%zmIgr)l`peS7HNbV19`W=|MP+gPSI7(t6{w(Or7qwc+7m z1}iJ8+7dMWWCjq1v8d&PZ=;DvMce_n9sehl-R0P`RQ$X4Kksn73P)o&G5wC&eM#m_ zM0l{OpB;A{Mi+KMxZ3;x&a+()blG0HrtN=7dj@Tvlj$jK)f3;mTNuE#T^Kc{pk2c= zC>wiZQ$Lgb?!(<%hx6;}>&r;3f6vC8KaclED`NTECjr%17L}0LIf(C@xu@kex^Ab5 z5F~k0k)16fK!t}iAY4-E@T{D;S)|>OTLCv6{ZFqHR1Ef+g;OSgx{waES&s_W_Bn}) zm^0CCJp`;xzT%@ZDKTxIQu}v`c^Hu;4=g_jU)C*XP2@LC919c4tA4MB>4>;W5&0X^9&Y~j#kp1qu182b z(AY?7E@7d7wngWkI!%FFqSTJ|?#uh4|L4+Ixu$3~dFOm2?w2o!%vC^P3IT;F`EY^_ zb$N&imf#7nBV-;60(0DbrgYvP4a*0s-Gb2={88%td|snsu^-&Htp|6hbU5={>03!t zT_b1DUhXz5yg$d5M*N_OTxI*+Iow09uhcAYjPao%zdkq`X&342r1_jP8ynD?x1{ON z>mA{pj`a;ce6>fjOK!NaXq=#x`>X^2*7%3KyyBGf^t$3u(x>e0lMd6nk@0=+)%+CU z`qyu+6m4s!ma=jE*3N!~4TBPBtmvA=Fb$hT0)d*SdGN^!;~JSnZ4WQXL+aT%=L{R6 z019TUMe&a%@X@CU+lN*!E3Hz%9#YdO{5}p?XedAS>7w|e&#~`#_C7k4bj=XZk)f)C z?hun>GT;|7)_#8J&8zH10fKOS7@}H=DegSZIUh=3pti!CjWms*m0PqK zPQ$wqfi~sh27kJq=9&({&+{#c5pX?+1Oh|)qDY^Mf}~YJVkLLcucx`B;`R#l^aDTL z%>6N!XAKE;j)jx?xgDpmz#;}Ppa+;ExhONYsrl8%og;P+#xh=l1Pap;+NfyQP&a*r zy_FYp&go%TA~Mr8UVYo8B5;R~53Ucc+-YuZ4k_=H4sR}Cg^#1l-cpQ4`al0| zDVp`yZ6Wi)(Dewr3>Efo!14IZ{%`_j@dhwtUwsf4md~DTwA$kJesTo-Vs(%sLh8b1{EuqH0*mB_l6Jf$$DiLbL)DlfF$Wji zkgYpX;n=Fz@1TT7UB;9dS3q{Bbd2mkOVrU@#Y-6aX3@7TYO9=v9B};-o@--Ek_?Zx z0;gtw0~0uR-%LmP_=!%cAXg)3Q58RChL&1$bP-`UV8mra{V+NFhNL0SK_o~GGHgH{ z)+EPcPiSO+<5cvJpJVWk&!bUPg}>gwcJwZc zW&~_SpZ?~}4;#DaCx9HM35}hL<|1xb^IDo4abUbB-eVaPapl2 z!l#phL@7JC>@*m$U7LXK+Y0qo+eng1+Jz@M{v9j(opc6ANZ zJ~>D&g7)oh=e2(c{XEH`{13kfDcWbJ2ER+a#t4tMa>QZ;Jz)uQ|_Pa)T`ajr8^~) z7U0?okG}sszR9F@(gX5xEx3e?MC51=t~0)WGx=5QUakX&2m9s*J11_8K1=Fly3z6z z3rs!fG5MD192fjD?A^tIhV)}r!ezw55(zs_XBhi+PxWrndRWw;$1q0h)`M?t#>7bf z;z>Xs^``>h`zKe!$d*=XmaHLxfUYy-$w<#81B_*~`%IVSsK z9d{POH7Z_iBgma;)5N5&E$>D&!b0J%-2FZ_3;&yceF%pMc}BJH*W@8322jouGDYor z^oNQ}cNr2tn;zU;1U63*wc7>gckiCo=H+S~Se}Q(I-RGnI2%+Equk)w%>o(-PATdl zi3ROf$A)~%`VFG4dolZ8q z-5(|P9bX?Q8A}1vZa9$a74RRonK~WH0Z#r7iL2wtt#GtRx`q4Tq-&UPnC|TgFNt+tc$9fmaq?Lw|Ng!*jSy3q(?33(>9BcsU81+Z3>6KY zxeNmJUz89o^~W=-OfmUE{w@|w0{&&F+TUZq@;XC8Lb$88Zn{Y9%YwB5)d`pdB@O32 zEvwgNdDzUxbFW#x>oj^L-!;^zmAlD@kqLtYi`HoQP2$l`>K4q|hQtEH!e>lAZQL+Y zUbA?uE7XGk1wO7sz~iOsK50pACh$w!nau#*6Rn?xx+gvoJ4&gdA|e}KM#}AON;BVl z#o$!6y!qqD4-dfq^>uEKe^}Fw@28d`CIvkxxDWr}CXgp%^(@bRmr~tG3J*oO6${ef zQR?(;agcbh0HvJ9uugMo%Isyl-R?y=L$yzGaIEX`Nb(&vBMcdZ%5dztSl23Tr@rMl{v|d zB8^r0UZy!GIsLh4%;;^ItIY=7PU^q;p0%;UFA+V5QL`o3g%)-4`71q4WTP0cQ1<)x z?{A(~_ko!!ipd_PZn;PXAjGUt0^Fx`#;d})-|oa?FVkF?Q+@D?kui3Fm=EdLVP5f7>FiHSN(Y+o7Y!8pG*}>R4kUG ztkt%tJ5^Rfy4a+$qdN*i#+cUVNI0zAFiJK4c+ctZnJ%?cx9HJ_GF%=21Z+k7M;tJTM5rDHuo(E2DMs4Ru0#mlFJ70GS#v;ZYOt}C2rVWTt8RPDrTSogVi%% z>Fgy*uO-jaN6o`X15KxX){^EB>iK$%*q`C+J@oBa@%YL~0(D8sOVO$lldv~xjNNIXJZ;_tFO4?*hNx)!$-w6LPp z3Zlrhx6B~b*g2tYH{7AC1chh=MYQjY!wJrPSs+^t0Y&QEi0Y5^&bWSY*B-%Z*I1cR za>KT2$k=Hobnu__KIvUvcm4S6_ zu&U0di}v0qkXGFcM{n)d4guT@U@|(Vy{NDAQM2{>_vxl4Qr^)q7+J{iKyO--)@?}(vhPu+ zCUD0BidkXD^+ILrK1PbvSaV9VZ@#oOnj)s_rQ=w;HfBX#gcrfEaM4yoJC$Kq%PGEe zt~^ipRFFCZixl6^&B_~9b;(7JR3dRpeFNZDg&`}4G5xaZ+fD4ai#nQ*dGrg3!wKKX z;!bNE2lPub4A!j4mTSIUjxfFC6CB5C%=Va@)YjNB?kK8x^%P57wENUUSyVTM3eb-? z9SUbN?%9*gdhLGE*zkP*{CO{^HebY(c^)o8l{d-JRHAZXUgo9OZIlskQE0h8xkn30 zH2G~BBb!g1EVxO`)lSMwds5R*T8hg1uz2f#cDd0k;AL4hGb`oF*MOLaMC2r+!--34v z)A5hFGO_ocCNe0mhlPd7vD@fatXKK%Z!oWkfTX`@)0Je!N&P}A(;G#ijWjRnmhnB6 zB(fHmmHv92tH4y;t|<`uK(fi(ruEp?75`wrkRM3NGJsk*Wd%MuuFt)lD_`SljFEXE z`bP64Z_%6a{tWDJo?5@>D!R_gRbeJ6(3YxW_>uc#Mu4SD%b3yPQH!dyPEUXys#EKOe~iJ|dFB4*%o~xt{hXRF_dP##Vh=5`%Bus60Q0 zS#G;@na!Vq(2XM~3$?+@v98Yk2d$amP37H_Zxi?AnZHmrgx?ugBcCe?*{$ed&+Sj`~;n>SWlM2WR(vg)1VXsy}UViuy#*k^H z6Wcdqi5$-Fa`-El_;Q<+a#t~2+7irxcYvPTB&>u1pAJ20MsC-kt>XK-zMzvGOIOSr zEXM4d;q70O>#@DP23P-$SC7#_IC(Fr>yefg9p{3}-=w2p2U6K9#>xCZX5$HB&>nuQ zdFW+E_66|GP&;-vM^zGfC@9@U`#7q4eYz!L())vLjybo0kEa82cr>i55DMb zTimF>X2G{TAhoUdIjsC#=8f%V)ycc4)FO>1dk+op_Doq^`R!N{^&tUhqJQPAR54gY zW?1^J>EV*jK)K?0ir{vV=8^hzuz*z8TZ&dK|3e`J3OYc6@=1Y=ghyd}uZ z=O>{z+2_Sz_H$y$z^RGMn?9c@YuV)`$=*~f(emxZEj+`zYe%@L? zU8h1Z?2Sd@9_;gOGxW?yw)X=C-L0{`Js#T~c0Yl%;IUPk;?IDvM%s*X%qkE(`pN#R z?z7N+DS~0JL{}Tdpl1CWX<@e+fR^(GO!Li7kz1LG*jBY)XCvP`rN;qBV8GyUCbwOj z^eBc?J0`2g8!o{u??;nnT`GKm+yc(zrJO5hcS8gRnRmBR_8{tj7ZlycK)AC|&_c5E z(-3cIK@v&UZyejSE2af|Gg4eDk0sPaG(4jC;iw$nJJ6?4W0^#)G`a*lZggX0FMZJv zhpOV-ugxE$`!h+!q$Kb%QpogNvbnl$+(PRntb8RO&~q$u|wQ|9T;uSvO+=YuSFCfOlwg!r-a@ zJIyWz@h`O>v)#&!t;V9am!y_~*7Da|2;r_E)X&cwO#LrlkOZxJ_Z(Cjfa zHZ6Hj#=#Si!Ksbh3^k$(G*7p200VnYyb+EMJhPh;1oB+XNLXhj;g6A4T90ms}eD%bMo}xxO z3JKUjp??bY9$X)|Q;K1WVI)t?_Sl$x;2k;!wmkkFT3D-Ay_8pJ(qC`Lh-ty?| zU6daEjOk+E|NBx2$rh?66j2u3%~1w72PH!AcncXHNmkc5tk9hz%5ydpV<$J!j1TQ8xnros`y3#9Ee( zgwWeIb1gl%HNb-iO!@Gj@x5*GQG(BPa&*D|v1S;g$_b7w z8`PEPOBA*XbMPKKN9%`U0F5qI70Wk+sJiRRnOtFNG-vs#@H#h)QN1#lDG)R;5a($_ zod%~F^rn;U|BxFpDlyB*`dy_FZ=zD^lD^6`UY@@-$6B7Mjv4*1Q-_YjBmIYkl&>~! z1`J#RKfJZ*6AL^!N*}g}?XVmOi-TIpf{p9wZw}MYpXDzMfnbDU<`D1wkKYN;qCLaP zHAnumcfNbu&n$s@igwkf)U;t?*|Z+&!}KdeR=eJync-&|XZFnHkD#FL`WLl;Gd|sa zxp`AC1%JUs660>3Fql2<&P98*=_Chqut`-&EQIsC{_8h`-m`FP8p7Ub*#rp@+<9;0 z9>h9@I!xMUXc{qN8lL}nrt%u9G-B31PlabS<{9AWX}2wZwHYYh2`*SgbLsB+n1ba@ z@-x-u31m!{Z};#=Bnm;kNuY^w&NTWO2HMWF*U)~)BjLH8VCT;~HzjH|Dkq`ybW-)Q z`%22F1Ytr2(;onpT5eK=p(Q_Po3_?*t)%5AC#70|u%f(j1bJoa(iq~a{A0*WfmP$~ z1UFSgRB#4CaD7zs4Z`MJXA-emaXfH%twmI^#&=l4U=)Q)aV7LWiJon+pjh_jgfs4q zZKUi+T9sDVF#lCIQXD)7s@&2%+Z5nPXU-{*vwCz1PnDQ{7QjOk+^wVq^uyPwsP6-&CV1AIG>4P`#3Py`@Gj?bwgSH>W_P=F#V&+Ur`-Y(4#rYexNs)h zf4SXp3eEpg+OUiDoTpYoJ=00F-)6S6F7>h*`h;z2zN$oGLfM~^W7~Cui{BrKFLcSwHx@mT+*Wg9DR-0)$w)?-inQ;nE zwwyPwX9QtS526))HtWna=PWBC-WiE)QjL-9IpO@?Leeq#76Va)c&*$!tK`lJ3lzM7 zpO#T}l?RE3TGn^TD`VIT{72~9TQ9DX!;8`R2T`KhEN^r9IS1SPxTA|ozpE4p?eiwk zpHqnA^5}f=sB>j6mW(=V^3+|F1ml#rgVI+W?`mVPP0D8ybkVVAqP5KD$zw?+G{Jy@ zqs_`o`?s1g@mRs#VE$e%oNqR#O0&RlJm=$1%_HQQ#DYgUd|P*U0nTx?s)n`j#c^?K zk-9+?tRyDst8GK{*Zz=9k!XPrR~5Qd5VUZKs)%L;YF#at)>$+{9SQ)|JaB?jse^j~ zXsPqQUzKo>JKc<1g`aJYWv3lZq07~QD1rR%<6D&U+1|7xm&|uQay%}L`I8~<>+xiP z-g)Wbyt=tIuuyS6iLEF-O=F!Vi6Cfvl(1MHn{VeFoOiTFj9X=-7cBo+h-s;x@-fDA zyqFan!1snvR)?apMmx}#75Wh%p5d~0TnpbwZwJfhC1RVJ?u2y&7BKF)lX~Ii%QA8x z_!3l?&8dkxEBK(4?Q=8N?bcRd;?+ePQssr9Z$z2V(2Z)M=xQ&(Edi(`+mXRayl9Wh znPHO!5jZnZ_D7`%z;(i1koE>>uT}VZ(3OixC9esSU2lxq?7Zk?&}79V3uDAss4Pgs z@*}aAlBX!gRWwhiyyoWQ?$p1U;u)~=%NyysYpT}DI6|m^67-qX&dzf^QHons;Va#l zh&2IO48cJA1@fa`gZE1Fx1Fv$ek#-(;gCUpA%BN!-EkWa$QUz~MHjTorufH#xCXT_xu}^dtB^*Q&@LN8*TGA7jr5Gg5K(uk%_$%v z5v))L>JfGr(CIp;j>m3r7PoupktDm0>4hMVzXWpdDDARJ>gAsrjmrE*r+{CNXn43{ z&myLOa(V*&<*7yHY?+XyCm(>$Kurvmqs68b?@4VZpeO|t=3D?1&Pf}6YgsBYip*Ws4H2hP$qRD7PU)2IEi^wh&M zy?^rJ)3V>31G|=Mo6zu%wz1FL-(`uN0Q~I&6)9D`3I3wiobRBwL6;RGw;9vp~q}OXRIrmi> z)sTUl!&=ix@|s^;QeOdylB%h~X|@nHujIfq01{QM zT;Ul_V8~KYhzl5maJ#BbaTM~sElslts4OWGPD9DZCfBpG0cEI(eB_#5OS7B_pU5a< z4ww>7pn&Rsx<>$Bo>)>)6?u@$^@b#oK860~Pq0h38Bx1Ar~=7)>$_cq?%N}?f-14d z`!lD*0iO-ZG&dv-+dO5ZG@~q5G78c}qbUC0NjTS;$k~EHsmH!8Nrg{e@0`BXGm$e; z8wX;upZ}v8$Aiw)S7!v=Xff^|4fpI$MOV)U*o@2U05nUOoNlel-muWcG^|~ zc7o2zzhYf-pLgKxo_YGN<6KF&HAsmZ9!llqtf-?;7d_T_w|ThTP={2STzf6n(b$sR zdO#a4&qI}NHxPk2RLRz?|Cq@w2L+Eld+<|TJkXe<%SyRuxsei1`_8XeZW;(O`ppt4 zbY2f%_0!LfG?S+Yj!>wn0iGSsmj zBg#$4^J_@;<$Oy?jwL%L@X6EVn_V{WPAef*oImTORA*9_yxXt65nrBje!PVS(G{Qr|I|!s3yVW z&b%09zGZPa7)edvvnf*HkfM4GQAm@#{KpOc5t2cja1Zj^m;U6D`!&hh#rdmbK%(n} zn;$JuU|teQ2$wRq<<5C=@A1Vj-ZPbU*)UQQ1rBnZnggTEmp4MAOY=@PC->y5e}iiw zk%ssF@XlSZW|8KJTIVDXRh@^oI*dq~{kSOQgoR^?Y6kpTr4iz>f3th6iaR2*^eZ;z z?OKN#2?<%cDAzVZ_Zh&!s{ZW6I`sG^p%gdH{Zio$91u7?r6s#=vjuf&74*09NZAK_EaBx4|Nmb#gIhVE~+m)b@w`sJ)k-Cp2yVf)m)uwWGJuR4yi zXw$L;hf!$_s!XHw8D1D!H2l3KB-?(U4=3F9ZJJfYBi{pL5cls#`0|2 zwl9#RoDQEP99M>+P2~YM8INkb4+Y~mY)(*VuOcsHkJ4fc{27bZI2gJ*t=G1)U%S;M z#coA8OiI;C8cxn=Zfu;Wf6bWOJx+kSAq;*hWksDGtk^buMBk2H;l%ytGi>b5yoO^^ zLhovm6A#WQi}e&oGz;!ZhM`I9Cx{pbPAkwQEmyF-N)rYLx37;%F)+jJqt;LB$jZj8 z>s$=vS4CacR|!!jnwjGYUO;47{bE*sj8s?&n?2ZKMTX;#&m0!Mo zCH71#%Kqh7irpmed*Cewyh4Gm_6Lo8A@>| z@Nz7X#MG}d_U{Z~ustJSDj#%jh!n2Z3u$v(h0u?{+k%PG@4uE^0uR$RB`aS$XK}#} zXW}X}k6Ib`u*Hi zTyUQ}c#d$t+A@|n3#7`ZH=4TU)aUJNKREn-PB<`>b*?swh2V3MExOLYaZ*kt8L%0# zi^JFTzlJAY%n%zaG96mh#`sX}BGc1!ANJ@)U#oe=P%PbX&z0uwy`{(>qy`Z z0NE9S`;^$$ne5^}v1cY>Tce_)oTVhw=yTf9m2sz6lu8ySOoazBP;DCn{_}*7u%9-u7w>2-~MzYidfeZBJ)9~B<-JjNTu{yPx1D#F?j3LN@qY&B5QkBu@_ zC{MZLzyroWF{qZW;I)P!Ug!ICfZQ+~3gi;E`|q_1 zj3#v7=K+ii!udo~P?v$}U6)M6H@E#|wkRPpVqESAOdSJ_w+7w)`HBN$?-fTxF8icXIVJ-!ZNpdEq1 z-$!V5zxToW4{kK&{%?iIv@Z9A@JPcu$xyvj5VQ0x{ zg^Q{Yd1_FJmy7svf`V-QZvART*?GRO-(ilDLEpv?TXXZAm@F|wZx1I`~;By(Ro#Ki4vUlHW)~O=h zhEI{7)Hd5oW>aRL{-|aETAbRNb+Yv#{V4a~vjH(H zFyP?#wZP%TznFeTTA|Yvo(kr&s8H^Y0*MVRm93+@D%G0!?O%)1% z%555rUK|KiJXwGHQ$JuMF7G4|qlvn5)3urMH98GD7_*nVnDqL}?g1EAa;omLNH1rO zLz}pW+7=s^bFT(iTP#>Vz;wKEO|!GFI6FHer(t!@FmmQ)q^^y2l0Y~9^zH(L+otxC zy-LKqEE`?|0KkIhV9z`GKhM(H-Fmiqg-XJ_2aLgaPFpa)r?8CV>^kSI4}{ona&OC| z4d1ps%c<+Hv)}Kkd!u`!$F2V{NUKq$*YS%5g{iDD3|%D+2a8)o#edSv6+iV}qq7)Q zFeM%vqx?(Bv*k@j!8w&BdNcCBKblTZb8~dRvrP2GF3BqGA5vP+tZHBP01dYN-971; zy}5Eq;)x#5D0A7L$GejWe>vFHV@gx9E(h`iaF3g$yLR{XguI&&s9nOW1pYfDy2hY4 z6PkUO>%ZFx7t0$?x=j52TY^_OZ7Eh?{^k;}nYMK}`A^|^{cT`ZlbhKu$?=1w*fYP0 z&9n6k>z+6TQ*NoJs$Hq&$~0Y1$kLFjt#Q3p z<@2vH9PhU5+qX>gHgITj!3YCy_y1|;yPUA%mUW1g-~A;DW(cjnZItaEi2hcH+zlmVUF|)4fjP$GmuuT|{NcEuGw3wzGtgyq zd%c~m|J~x-=&NG)Uh(Mnwf&KuLi8~LaF_kTqTT$l-TMa9{ww|JpT2m~mv{@T*6&X2 zPyF628+%ea*L|id%#t~4#jVM0e>L!*uSuDu+5yP4Y3L+$fcL@J)?Qrenz^{zUccgAQgJ#X6Q!p7V|-wI;utTUrL*Sk`-SGn}H>My2=wUFm~ zzuyp*40HAlf6yL(Ebr-1?i3jVj|_imn{8m8AFa0$*-$wbmHYt`v zaBXE+P`>7KS8q=&oNXlVcqnkL=Nwce``VG~w$k0z7?rE-L-^H(!zcOA9+N3=nf#(7 zC4r%be(NpL>_I~C72+Ifh07g4WPzn$_d-BoXb?}pcpE5W!A zSphzJ#X}J-va1Y%J9Y}2DbHu3B&*!n2^>V6zQ2P{3$Iyke|gM$?CzgmZUmO^LFWjC zNex@M3$F$JEQ%9XX;*|1pDxyHdUKs28`twlo~CX1N_*XhXWdvh)5!mR53^3`7OW+el!uw-#< zY|R&RR`nGRM+kknNoJ`5TRAu9RM`kydWK|c#$Fq7S?Olm1xC3Uo)guafwr}!WQP;G z4N1h4NQ33Czux%1$s$_3VM%1&&excWqU!FNM0VHl$Y=k$=Mq2O$q6+x!(cdk;U5J=x`o z#D;6N1DfFt2LmMn%fUhLplH^9*Ac_;L-*Ch>+iVtJLSiF%(rDH_j~;6yGc7phBUtZ zaYP@QydSC(mbia?){gV6vH}oOxIHZAe*Hc5{XSKsTAs**dMOkGg4ci>d! zFWw$s4e&fzd!zVk?O$isG4LA%ezhAFT0XT$_;*CuTK#vj>O2lL`fh{c;1@{Y0&do8 zGJoFvinaPt`eHiWt*2di?GuNxe&6N?#pB1t$eAp7snRk1m;dhgX6t#X1RK?$$lV}_ z6~5HUCDXJfEx;rZu+A`ZV!321_gLcbz{LqpiM1oJfv&bfA>xxEzYm6yk05XFK+W)* zUQ;hvMpVCl>2iy5zu0cBpmJm;TS&S$B6`8urcQu*en@U+Vm;dP#z}ojPT6`mp;_hT z!HljosSG<0qMiYboY#N(=~%z87 zwFu4=ui*t8kq_*rB-0Eo@yx#w?Yx0m=6xx7dp&1{C86S~AEpe~<5gG2)I; zoJV946mU=6qK6o&cDFns)%0s6m5?LXdRoCy$L?dhjilYU@T&z6_xO58OYgyxKohw1 zi$`HSOnRZY{!T7|E9qh1V7pk@m;2Os+ySe)`@@{!hpM>-<@{s&A2XhQCHIbGkNH(R zB|OkjMAp3jZ{Il1oLP+YXYD{ywi)W;hEFpmYwSxElqW`M+H5wHkI~S&D4Mw3oSjKm z6qKUTH}W!Ep2{1N#RV;U(8y-pZe6yDmgeRuPA%$kq+KPHwXvqL?tj;q@0P7ooIDu( zyy1(YmHP{ayG0q)55sbH`IiE|tFU4uudhAir0g<6yc&B!G{Hjw=iJ_NxMnSZ|L^?#Pqs^lq~1KWO`1sj?`qn#vlou{Sa276AKx~-ES~-$p9x6bNLZ+M{+Cq2ZE`*X ziX)J1WG{$?Yss{ZSgWy}{nh@?HD~&$CcD8%me4d~hwk5XA?^>RkL_gB)!({sU|&1B zTz(doq4tB$Xr95m`gv;kt<9`|b%jHQ9u);gZGlH^>3)xmOuMpM8G`E&lC2sN-`6-S z+_!o)K3o>~!sh?`GWwR7>*SL6pYcYit}m*UA_N!2cK#Ql|HAz=Wb3M(+zf;Cdzh*) zjo^~$ZNQ~zvn+|<@BXFxs$=~LyK4*K@6D6nH#fw~%HC%Xe)9h&e%oGio3U#RVNp<7 z){OVuTphWr`91D=uDHamyMbG$`g+RLW+`Cfvt^08TwUHW-lUhUgshGM3;0z>9>b|E zQ-4-yE#R(qxBpjp-wAKxTn@*dMmE^t$|jq{PH6+rg7(JHj^mc$^8aQ5Dv*v}vcLlh zn?Ix3P4_19>miC;+!Lht^Vf1Xk~ilRK$@OKWIr*Y*bcWZ=_?9C;n{_{LP$-tsI7U& z4z-Y>y40vNWZ8D_+W@i8=lHB@2R=#dO}%tWTi*q|dA#AqHf1XrhVwb8)M9>4A3%%+ zKa}c+9sz!)R_+?>wQ2eHh}8A4=<^wOe!QTqOviVpXHzd{{8fvIb*n|$=?1$*9wzL*$fAMGdtTd}D}rzVPWcvQi3Nsu50!Y6Hs;sW zR?q3(RbJP;KP;q!`zod%4t$WCyDjb2Gty#nX*7|hsm7jQ8p8HSaHbyHvIgLHbgSgw zK}q$p`3M!h@<9dFk?+s6iFm?64gimVh4JRYXMHeF?ghoYrc+C>XBs-)V22w=1}k`% z6AH#X*bP)V-FpB-v|{}kOOz(uL2PMWjXfhU!^|P!=xY0?4UqEmSIhlwA&P0$UmeXh zQ=iYL*v*Ik5#b7Oj9r__lmVQ#?=&SI$*p+oMg_F`YIDRAW>3p1;*3q&y^WM%-_`of zqb2te-$yHpy~VDHhtvCnz<>#_Muy)u@X;X0-V`rb~~NjgbnAMBnIo3S=7g@}qDT&^B~8YPJcE3iDuW8@s`%altnpammCF*JX>$`AS+aK+6jp`Q- zrKRuAm%Vc2BtOyrx25krbJ@)Z?wc=Lh~5@`G%uCe3?=l-%v+UFM(NUI?iN0|y{e^- zd43IY(EKV7SXk$ahtA>cG-s`X6_@V22M^SNidgtFx&P?T%t2A*^(X@dwKFk^12$JD zJT9-2JAZ=X)0FPYx;Feh&DuxaKgaEit1W%=|^CC9fX0%%5%pE)NCM#x*6> z4x6&oWYL+|(hbHVW^Ouv8bPf$^C$B_|mu;49vr-aQyw~}|9HW^Rz z+FFq1vf0rz-ERNr%PbZvzL`*Rb2!!5iDR0V7~8io;s)mqmbDf}Y5Tm&s%g>8hqkXH zyf4o8k&K$R%9s5gO;;Tj)${cgk+5KC1&IYj8bP`gSyDhcrKP*15$SGekfo7M>5xw8 zmS!ml>3-*a-{=cO%$!f1GX`;#gm>9qsMO5a^|hpNOfwLDQ4)zF*f}mxAM|#+ z-quO?shuHM!75riUGTcK(h0sG`zvW$QV$BQH~PNt&5H0kx=oa%EY~(T>xR06 zzH84PwH;nf4|zJtT2xcJ$l!&XrnXy`SXrUP_0g14oRkKD&t7!4Vp61NUvH_R9?Y<- z{v2G5if}g+s!uw|R-Pm6B6qVO&#eakMx?kTM&eHM;(gZrDKg2KN9$Tu<3T_t**l?? z^q^WN|NO_rxHvDquEVlzidU z<$Wx?!*rIRx)^W!NUeTSK{&pQS+1(;C3YVVoz35wDwlxN`t4_3ZW89rbb_0bg6U_E zp@VERKQ}FjxPthjgM1BZt`e4&Jvn304aAv2gu!a-gf?F;DsVN_w4iK|XC>4D=&Meb z(RIKP>-5giY$#2iPP{&LtHST(`Me;e&jbdLz z6`r=KJZn)QQ>>v~ZK2M0I%i++!5gi_5PGacXGefhhF{<*xUFjT+_rLq3B`>?X|^RMsVU#A3$%4-KVr=r+^mVe7aX$ z&w+HYnU@*9bs8IyU*Js_+T($#nJsE*4-7#(X}+f)`E0D~Zk>Vw)^A zeGf;V6oudHZJX>%Hl9GX{hI-=$vD+fWdUHVo?hw*0rqA&D|hoJ%$24%bLPf|Lx#Dj zPEvGf&idMnuYEGv(=NtxPXKgZtZ8_c$h4pzYY8?c8cJxoB+?97Ma>icXw@~f8D_3`F6O{@INX4H9y5w5mUPYq*~!$wBH`oN1&%v38**^o7{ zoA+r;pcd%ua)C;tRFmIODSl+T4Qy22WMRu6)-=y*ymlE z;mwRU`{h*Gzh*DNz6Exp=V+@Wo*S9z@8VwJG~1M9&tH$ZSE)_R?g_eEolWoG>D;kbJKXL<%_mhQ5%gV6ET3&(x&p8fJqldE|b;e>}H*#Qg{Edr6Y-&`n@HC%LpJ9#=t zTB3N1$Pa+m8+uM^59`!kw+{{K{E($zY81+ws@%+QFQz$K2@|cOa_NPdJkETi?B`7Jf`6|NY{ZcafRKu@E=YtO71p|Q% zU#}aKmnDQ4I6R_4&Xo!XcZ{b8*Ax5XIMa5&h9jp(%3 z7O<`rTBmfEJyTa071`Y5#-cb@t0$ zo8yoz&6cLhjC=LKeWl`;dOyvD^Phjy*5Y^xhqFVxsFY$FFG)5hw-sCiY}7mMf4oat zvrzlQ@9neE^qbhG%lW;W5SF@^USdD3v5LFOXy*9cU;O<16D3(aVpO})>(I3K7V~+m z=k4Xfq~KsQ8;cz^HYpX4*O}(Rg-VmSv+z%GGvUnsd$n>d>-!ZvJEgzu#_ghpo_E@G z!lGv(p;>Q1HQM#@q3SxNK**GmYCXNC!l9bDz1Kn9R925poHuZm3huoPPQSfE)qX8_ zy0dGndb<=rkv6mlKT{=eDvx0zja_9`Hf>0kmyu!n!Sy|-6K1jEUV~&rdLIAK$^Hy9 zNnX>^>~C{(21?&xlr+v)<0YP)s^!BuwHwz~84c#_gYjOn#Qy)oA~M409Nh zLBAmfG0HFoi{(L2D2TOkUW1AEKBo`Y+7L?>635mYJvx6>@kSA#gRQoY!#B)ZphjYMV9cAgt}F73LB+iwI)8S|Qi zFE3tQB`;jPh@b|ai64aw))N5W+W0g zX!i#Q-t%U3(Eqnd$L`Y5xxn769aq;hGDFD8&)u3qD(~$4tV{UMsdA`1ed||09ehzF z-|&6uL##acMD11e&sPExM35ZH8#&oM6oAcsR|+iyeN2+4ahz;7XgU2j*k#iBD6*R>+p!j7GH*O&OXon$jTI+aXbJdzr!sx?zmr-dSYqNkV z%s$+i5jBj_X)u^&cvV5>J|z9$R;h z*CDD^=?-ovZVxD)%UQ|GEQI{c390k?`8n^77sT>3NxAX%SA-0Hk)1wDmPtG%7}rk= zW3Wgnq&x3hzgrL$QfGX7*?i02xcza@%!F*obGd2R^{MzDprho_37_})%~1=~t9cCz z=EbM+M=Ql{{8ZLFp51#zldt%*S^wV0%w$-$lj#r@=+85Sk9<~5!@s@xuKBK+^0DrBi({1B^k13@O=zJ+DZo9R- zwF?&6n533M*PC*=&^hQ=wY19fINTu*lR19#r4oAd58x?WCX37Z;&H7r0 z($EJDhz+X#Axl%WATlw0k34pWLe(l1^{wY1(fDX0g%5({RwtSf%k~+})G#%r@ng_z zd6*NRG92aWv>nQfpvusc<78dIIs6O%aQ|bwR^%!=^TyNk;1&&JUV)k97HvPN!IgoJ zy!;`DASA4eh=UUZ?Ms(#j+ih z!bY8{u&%UBRHf<{;ZYm6W4S`gXQ$a~g+`q@cm)r587uOLBDXwJwx#M8=YG>1T>G zifjEYD9sz7vA|%`Pa`*;^nxf-*0UrQBE)qHO}?t=IIDpG>81;p^GPje_V_^aC9$T| zh)pHGe7|2uatrS(s?y(_CXg53$%aL{;b-JF1L;>gbkr203qAPz>$gmwM+h^!@4UKe z8%O5CfZ`K zVf%q6(BpIH8RB}2b%(ukB>1O<(v3WG)9uYA@xSW?95(BMT!n8B#0F0d57l5dU`L`v zEr*AAnkeRVc`iQ>37#^oy15pDx?CkKNb7 zD7+q>G?|?XE9FK~t&O=i-!rhZgqhQ4m%MIIy+$EJO{6&PH0O^MVPz0Yd3lNq4&ueR z`Sm(?vm`hH6=mUBgy{D)03k;aY3z^pL}?dX4~Hd*#9a};a5IXbwt zv7?S!c}1m@QTfGxTLR0FfRIvOWFf**J@L(W+M)DXhVdxDkH7)U{AV9BJ9PNHgi<(M zFE(>g=p7^3(wZ0jqkoQi8B3UsE-nb;oUC2?H0_o%^D0utXL)8vRBUepP`sxYA=lv1 z9xMHMbu)azi}tt(em+&a<%yRg&-{nsrb+&|CVSu|P2g4$@(-S8weaovko|Jr^RpWo zFA6~AiKDN=R@mR)SdkNEBWU?~!EsF;(ce`^a4dMPbp^az=2f?G9~nVJ*k5rnThS&} zNqu7P(^wj(#vlvHk@+ciac+Wj+0a8WMaq1hIRjXQ;jU4Tm! zDN|2_bt4|e>A!l*yfvrkgc~DZl4~-pszfP&Ox<&|Ox(-^)$CnEW%7`ZV@|4Mrys`7 zQ{uJkB&imR`Ormp@V*DGvKpI~od+;TYs^?Z=~+MyR?^kV;nk;fx>U%DAe|$EnlI zi<-3d7xqEO!NA;^#(Cw+l;<9R0H9=_6(JGD*fq>6jV$GHr;x2KD|kG1G}R?kz4nB5 ze*d)jw()mZzS1oO68`TBgu34 zzWcUl4H0PnbftM$!Q76$>-pDQ$6PaH%kx)f0D!X zT3PK^XIk;^E)n?_kpB|ThP?oW&pkNT-eF@zDr7xM5Re~e*Y|5CjsxS;rdUXcd=M|Z zuz7xX!Nzi2QIc}53KzhN+&G`TXAvQI@^Yt8oq@q^{*T*U>aNG08}Z5YmSpU|6sCeA z)>O_J58;>(@cfU!R;k~XWU$0O#9G%tEDCOkV&bJ`QUGV_-+G~urmN!k%HdB>8sqeT zvAF~nf;B)lyb^RV(gvLMT=9h0$IG_p3W;SCi{#WfL|`1vCj;p1u0r~TUL3Hx zLgw-pn9=$2S!h1Ahx4L|?53HgBfl=^YfN|es$Pe+4}hRsJV@1~uYRe;z)8Q0cMm~J zUebJXA<`sQ6Y1;Ta*zDO)O0y$;X5a~CB@@Fge|2y50;areUXsw(a}zRhwIM~71H*i zQr1#*rP1tji0O4Yc$=6m!!u=$H?`4#9e z;$<(ts>%F9yUURuss;POS{ic(av(qrGNu}6JHbys6aDi^Yhoe}y~hMcB?3}bdH0?B zUv*_cCYk91u+#OGed~MbmC`bcARhA=~OyvbJ238v_e|0x^*dqGOU^ z8K~+l#H-|`J|TC|X9~414gMgvW<4zNa%Nutkf%2*Z{>YbYiF#YoM6?`0DeC&Z^Q}Y z-tG#GT9vtYzw(t*)AnBF#MUdYo+f$6o=}mziSavsq3IyqVeb@Ms0lu7~b0b7kF`vrE0}c8lJ?5VKeIi^! zQI>oKk~V+*A3Q(i=9+%)aWuTyjxYBfiV%W_WGGfb>*eCK2h<&xhGq5tklw|0KH82_ zbgnAl^uIc7blL0LSW?t>w0jOCg4*&FD=^076ZKyx$3fP;O*DbsAn`?PlA$<^*!Td5 z++O`9PF_U9$N6$=RsVMD_x?->8{3nZtN%FUJDD;#6g&$MzM^WzX^_A;j63b-^Z^2O z)oWYAbxDB*68?E6YQhK?DY7WN@si&D!bl7v{?PpJ>|++mQHUpbjHai**{c6@TufG& zEhRpW(BzoO!i!z*XJzn{a3cByCZ=y9Ls)v>BDLFngYG{#H7u~wx0#$<{9g2*m3)+f z63C?{A#|wFtk5m#hZCAt|98G$F!TC2IE5^3gb@!i~kY z$tgpsxVRpj#{y3Hkf+pbeE0MsF~7N>d3WNbS$IWr)IsNZjUXb;a!n{cnHA8>nHIUC;_c<_aV==NCWHm>@Q>o$lxf(aJU zyWR%@wh6?fawcw9vAF`Y*Py<3KQ9axqDwjTS!EZe8Qw}C(Es$yh(;S?)}v5C^72ec zy-FN%e?dpxw2}C~EWjbH>2FbS&h>!M6&laW>=pzA{zNbAm*Kmz)5P9>uoKBcObcF7 z{EG}&eZFx0+N6Tx3Mi>gR(#e?d$F`8_L%I|jk)~ZpTiayyI+xb-dXoG~(w{>$_ua$f_1>2n2YXN@ z#fpMlzs(hjTjvXK_8ZccW_h>0=*YXse(ss+_b__v7mUg_G`)k7fggmV9bq!x&lq*i zrF3Y$roBZ)l6woCUQlAbt&jORppQHNUI}-N;tuEgZ+Q$WjcZNRwyQ3$QH~Nl*=;PHgl{sY6=+#Om+oS{-EyUVM4lf&IePp8($rkaGEjB=X(6c5#~ z*Pl74HhV&3Xo|vVLkHZ`Pa9Bzyg~bg#)6D=?R|X4E2wsVh47j4Nf2E;+A$X9Z@j(U z8xrTA2WFs4q`p zPi30xAW4k71=?5V?0QH(*1_)Y48^mOnQujrg%*-jj!#a>-p)fT2w09p!r~Mj4tyY> zRe$56{|=j(vhVAJxe}bvL&d6OCGYW!pW-$D23G&|#bmQAD$T7Jg^;8@07}lQRQG>9 zwS;aZbMcj1P%1ldXfu2EoxJ7Jw8@1xjyvTLv5E8oBTvZ<6n1kR|Dhc>WE!94iNFgE zJGPxm{+pJ4612)XH8&sP$McLy%srcdsDJ*6M$Nq#q1nbGx|wRbV&xJ&+qUAd1h%Lz z%N%A^il{O+2l4&I<+S>%e|@9V#_99_?DYelJ9Udc+OJABX&u|*?L=g&3h$l54gAexqdZlt#3H)**;5;ROYwZq{^eXa$#H5ifV9f020; z{f0>92uOsk30NJYF?h=f9JC+(C%}TJ1jyz~nuTn*zY&NrCQJs|GKmiAZ<1s&ZLgx6 z2v(g-@^>y+M`<~OoK#-C9b*Lk$wD@?;{{zNSmSJ%WEHB$X;HSvQg4bDaQp4&NG;GY zO}M2a>j}|Gp$xvmn7nx0iPjrOiewL-yhMF z^r#eC6eX`jbRl|Bw2~bz3|ACg{_AQA$4g$!o zOZkQ6#*2kGP)ED-<|S1p8Vi?x{3Fkh#A;TxYvn#TLOhx#8Gd{o^l-SJkLCTh!5)Hx z^TlNd4b1R)b$a|v0~y!59Xe5Xh%vD(F^O9dGu2HBi8T*E@QqLBKkMMhxnV#0AbPz_ z-?fgO3G1|mZIXfFxljQG2sV#~41YtIR>8HystEgKst@M`z_uw6S(^squID%VHhBat z!v+3z@6>GKG%Z+&BxCrnjqWo;=XH<$ zR!n-k-G6C&2Ceu>KD4uf|z0YNRmqnHE3s>+K==6r-&mEPjZ!WftG0AM+oNQ!9 z&_U)$b`0R-DBfxyx96dFSrWkYff$U<&$1eUc(HeowN0?oXTiOu#RPS z1wtj+A1ty}f=a`EHP_PPgL=-IDy|I;n_IrN#?~C1^GG8?3{Lv$U}|)ft8Rf zy1j6<&Mh;*8~42^aWdWO=G-9#99NPyFM6FoSCur9<8LeZ_wJ+7FcI}&&>to+@CzC? zz4Vs~J|f%J6uqg&I%0>cc2enGG+ckNNl4)JX;E*)=n$kjhK}Rzyd7gr_6R*2nblRI zD!-$*esj6EceM5z9fN{MiX?-rGEzj&C&Z8zMQWzXTH1p4;NElp*Wa>M1=nH~lURsL zr;U;coj#5vQ@(Ek_@YM-%9uQNUW!+m3|s_pZYQ&Nqs@tpI;WP4jCpgX306e*VBB)+ zozvAAIU^jwqYtyGaFEB3bAVL)X{L()Xy&>N50O#a;w;xt=3(RnZ z$0q}U&3Zai*Pg=D9EiHdWZgDf1}SB`4h@nepw`}d+_ndHPAb$nGwq9w&*#DG=e=JK zT(4X{xsiHY<$WM&ygHUQLsN*aAl5VZIw=gpbw>VcbY=cj75eBj@xE>2snc?MkR%Dx zs~;>&&c=Y5@+dp;;QN^PBfWdT=8{RxRTTTU`2?5G8eH3;M2rXfJ_%O}@#Q^ey4;n9 zpE1|WMW=bscY-JK=ihnkVTBMg+QeR1PM;OQdgV(^syMyE(MkThgS0pT`)^LbCi=X3 zSP`w*23c_q5cQT^6y3?{WJ0nnquz52#18On7fxGL3`|U*XwqvyBqmqs@b4-pFfy3t zqQ3hj?rV(s%sgj_CyO)Aw9NMP-GROqDb}c0 zV|hB8v|!QbwsM4r@sc(J^*VnvUXjG2kbBPN*AA4B=DI!_{d{4Ub(f7?@Sq*P=fl;a z4^EE5&|fY)BJ8LqOtIw(hW^_5*Vx)!b0{&cG?I|~=iG<=9Fs?A!Qw|GQRc(87P$s+hW(eTP>_kv0zz0|SY z9V)Q5>HSe4qBs;TcrqocBR1J`)%EuIT;c1%?t<2vM`bZ4Qvms~X*w_Y%_`s|U+dL( zEPOe*01G|WNAStvVwsC~V{ru?}fiUzhRWAjaK zVL&VRqhF!fd|fBOCZS8jV&$+RLL=%_# zIrB`DDx$mG`8yQTU$2Mb+pkBz zZk~7EdxQyUi7dP{3cKiT$*~wZ-6OJw2%q$omc_RWuNww!C0fqBNdgvUaoUKkaWjcb z$K(tn!@PvbqtoBVjQqtDXC8`P~0zsBWEltyA$vI;bxJ*m>7^GH8$e)`J)+zh}i ziSUnOG|UfX8uAFd{svxkXp-yYVzJegexty?pgFC@dKKpRJG^pe4s+r#tGy$t0v1?I z^COC$vYz;Y)*!*BoCTqB``e3H3qh~3bWe*b@Sj%G+xhm+Dt21q-mA-rtML zvv>enrr;qu7~nk9w*y+YI4$SYy$))^f^F^|u7KT@F1m=Ed6LnX$NNa$Z?9X>>b}5& z4F=;DZ&%}wwVPp~qb0aPH$0{4SH7>~EtDPHff`U0QM}uQ73F~X7v%Q&KYCF3Yl_ZB z;CM`N^C!sul|zvVP0&ah!|?`YfJrB&!0?&BQhW?7DKQedkH?@nD`~`#nDdEx#d4&6 z&I8yc;neNF_lt!5cqiFbI3}=7$I^TL6$yhxBOxzuf&<6yU|5@-%!9B{YEx`6<~>6H z#6X(1vtD@qya;z^CX(yjn7;X^>sNz1N=I)6O<7=$xbR?!zkSGrI?Eyqs-`>%lI^G` zI8g>r2!ox->%32A_h@Yly^R%#DKElmwPvxQL203V-e|~vowF(rP zV){?ShwR5ClfY2V5w;&j5X*=RLh8{`t!|qyHYcKa;=vRAA>mthJrrZ^MAB{*?oe^m zoA(B3tjp&E_RsFt$Laot&YWZ-Prb}Au3qRIPwJzW`203?5oG$AvIGBsdt^qE>eSWv zs|-?AUhh!gerX0w>_3cE4bTSBdSp)n0cRpYv@X>~G-RxT&pD{_TQ?e^lT2625gZ^w zxWdb*8*X$y-v!w$vA1U(Y=iKk>6h{fdwOSuMR}n{$_kin#FFo_@+h2qO7A_S@mPp=HW0!u+7Of z0ch|vlL{NqsK`5hv7#ZGMNH}Tu77EY*XAonNSuVZ-dxl_UdB&8uSc|9VI4X!82zJ1 z(t%$BMMdvWU+90|MVA+TbT`2y$%vAZ%&`hqsadx8?uuFJcOiEhuE7lGrJg)OmA`zU z5!5}WlH*JJplbT^DFIc>5q~IE=#Vr^tzpKp4CC8l5$%b5xeBD;PiEru($I~!O;4UzUL`e`of^i0PuHVHl%+Tikl^i?^)boG%Meegzf zG`NSCAE64#zbh+(c0$n<(D=s)fFdf1k9bIBo$V1UrA9rW8P=u|h2{v13g*Izmu==n z>ZE6y@V$^P#$-bjX4EwzS8L?uL~pcM(2(B2+9gM%P&acm=uphTyL&t~7FG`DAueYT z0C+?*NpiJ^F5P0f_Fnny4v=Q2K^Qw};@|NEiBXz6Sia?>a>9c3VA`o-@3BhDltF4A ziclF%R5WWg$YDs`v}x^pRFX8Lo3dpl-I3QEK0#5WF&kLb#t5B*d zry1(Fq(H>DBcKM36;Qek!A-Ds5D;12jwfySkf{`ytWRN^TbxocUt{+aOEOCEurRSm z8WjcFvEEfn@W}D`7FeG%8SfJ7k~dY?(tUSqhpgRq%=# zy?r`?L;H7i?)S@B4W9J`gJ_>WAP6u~exsVTsMo6m+jJ@=UZff13qFr94jcL$HSy~r zgp=HyZYQbuX_o4+D;*pzX7$EE{MNM%I612#`{SyS9zCt2d05e z*SgVo_=uB@5tO(t2Vc`#CjH^Nwu|kObcHwE{Bq0tim{9BRf}2`fYK4$H7x?Y!u6sF z;eDoQK7kSXI?JQDhD?N0ZN;Lz=Y`*=bgb?><)GpdXzaxs*DGNfXv2^UV&`DCe$Kva zOm5yqbttA3uM(Pk#&kbMp?MI}=p#Q!9}q7qsXdwOPO8mw7(itE5yMRH>lyS+lC9xJ zP2d@@G9qRbmorDJWunC7uLxmLIbc=2;UhpM*0oY|nne;iGMg7wsj!<7%EwsmOqJ0; zdtl%u50ei}6y+Pg7ou*aL3BS#nnc7n#kR9;u*W~XdI88q$y~A+5d!}LlTm!FQOjeB za^q?Y_b>->L~n`W24e}jMW1se@20;BqWq{tLslgO-FMbT7M*7-NArsQQ^=QU@v*|L z{1DC-DAy_?L;=mVfKnrjJMKKO@|e$}8YIYpi6t^LX1BY>=9ZMa9ebls)LGXfmNk{{%R}%&X7JM??}|mOwryeNw*q)DGT(v>Oo2E&Q~o zAp_T1J-E};Pi6>6KnWkgIrv;|lV!w;?dLo&&HTn6=m3E&S(Z11B$!%S-n6^>Pw9vh z<0q&Id9>|uA~WJ-wy`@E%E03caA@VL=d#HNUZ=7rGzg*X z7I@|=$mdV2YVmZD*%R0aU3?j|fR7aEYPQy(Pw2h>?b9z2s^{@>bxKid`G<-#k#)_L^76xVqOFn7@V!%%TElWw z(>{GuA8`R!X8t&`m)G5=4VJrG;c1ig6UZnPV3YpLHf2H7!OCeq+$MET8JS0{p!;&b z!e0*908Qr;6!UlGB5d6l&Q$b&*uDS}+x%i1LDNCChq&hf`+a2ctMaBb#S(16i2!}s@ z+dlHtI3=pRGjOZ*%^XXF5_$u^oWLuD{$tE#re$i-*foY_a8Jzl$uMS?g(t}1mXlgH zys8IjuOH94gLYfL`%_uxXMdB*=L{k0-UH9ae_Q@vlcrTI?JPj559`UBIAHan9$=?h zO1>8!xzR4#Q8x=bACega$`T>pKh& z@%k=^!H*>_-?ux-!x^{lD13&#+jKBval%XhN{Ts=8y8cZf4?Ey-=iYGnX&b-8Xrj% z{^cl9_LqGDQ7*@g|FfTnz-ILxdb|M4iR7bk@S2!;lg3wKyDl(W;}wW`)R$;JDE-w+ zp_K5^#wM#*V;!{)WM@R1@b0(nW2v{WOL|LC!flV1UkPF6)tBDCOywsraD=Z)F+RdJ zrEIbcJidhaxg_T4j1C4@Vp_=U=&3a?IqS7I9`Qj~b_AEiQA2*{he}{@%c7LhOEe44 zDYkiV2}Dn%|Aqzu$tv58JtaDR(EB6i-I5T#M3RwGZGIn%goJ7V z7$4tKyl`92-fS8BGQ!=!-vvAH2l7I@Y$RoTry*R=`#m1dUIP$sAE(~?)eiLe3`8Ayk?;>C~y1KQ&1>>xeAFCbyin0o!doqDTYdEcEnsR4g;Gmr$e zfe|=D2lwR3<&n~+^dAJ&-4DC))eA15@;FfDu{)_zuy?9ou0B4)8seeFMGW$2*tFh% z_}RHnYUnS(wOL^LdDs0;?(Nq|ix9N8DUOt|U7oz(PYS0K#YyPS%bDixkztwOjC4{z zJJg=3H2JwVcZ=K-3JD~@w%5yYGx93Q43==yWp2}cLB{&5swBt(|G#%uQA)1?23rIW zdR0Vw<75({+5FIr@n5Q+FQun(_3O?tYIzg%C3Ltb5Np#e&`D(Kub~pIn9zktBPaFR zp{_3v5GtcH>bjo=z#Ch@oSmrl33$pU2~#Q9A&|BE0v$NCWVRWEX;;+n4p zyyUCz?nb|qmYIsR8Q2jLsTQIuilsx>~(A8O4vFq%IaPhDOqosZRxsRnq4~W2sv_s^{F^~OFN6=0|ZpOzbJ){neEWw zR^;dME6lHGP|

%V$mQaI>A+---w0#b12?!x?1+_rVa+mya@lR9h+7tjV?NBVU1) zVlZ*137;EQjO!^mKgR@tvxjclPNY~Gb6%NGw^x9>&=kb#png!HMe{QPC;!sM6n@Q%N?0> z0!`RTenpksf@ zhKYX;(8xVUF7-ZHU>rPdOs@?$o`Y(nw%fdJ_pvZA3N|k(MG22ROp*;E1govLmY1iX zD+lSiiah&ghM(9lefJk!b}5NS79e2(uxEx~Kr=$|j(p9EOz;1)0IQf3?lyQ|FR@3| zSI`S8k^&t_Mal4vFTo@ks_SIV;3_f13?_X#^;W#Dw`X4#jnBY;{tTk^g(E<^rarUV zn@{sNN~iQgK7jQ(dg%m&Q#p`)?@GjC36Y46<|c8LGzq+SiG zFrCj^j>VRrKvu}nh&5y|q4|pcj*4D0h-@hdu#f7T;(Wz@GS{1PR7GZl5Z?eZ0?+cb z*;t+h2w4r}q_dkTr_HxK6pFdTx_W>Q?zsf$PYCeHQaVj+ z6TESYeEa2~>mulpX5%2JsL9Vg&z4M6LA{bG9TGe1z(|NYId8NYBG}}d>gphAr;ki( zhF^c3;N2t*jD16Ka{8hm=;)e|~O^+$r);N)h;M zRkxY*&GQM8WSIzwYii}z)BUcUx9yL-`13V)P0B(-C`vh2+;E~lhAB%uwnMvIGwryX zB^jg8XgQ(4GkmP^K%qE(3>X()?garX@Da;J-FIT_qI79*vt~apFpOy1oczM}X}n`j z(e?d^v~hEg0jvlbEMF)`?Mi!Wz&$zHc}G&J4R{yS*+o|IW%03!SS=(yHNcaz04*m|Z&AtmVQANCiOF*ZpJQTa(2?W0b zaovTG=*Q#ruq__=l@O4XP46-P4am!c`jDo>jIv%Na|`W$MtFs#O5A-=C^zV8X7Upi*#(_>tCskpoF(s_p;K3E8vSUF0PO>K!g5 zYbd%|06zZlBPLJEogU9O`LWs|YD7`3cLK~ra|*ydFv98p7v ziE~7n{w`*a(cZ)1BGD1uF*s5W-0a>Dnaitwv-x$|7FTM{cz@l1>ztMMu)SaQ_$^Sr zpXH;!8r%aTcBajK6n&Ecuav&7BId6z$zbEJ-{OB|Lcce%sgO2yBAYZBfu>cIn_?&B zv>GLz^H`bVsI4*fcj^h z{m50hI&tLxA*NIDs-7hg+1NbFZ2yN zzBB`u&j@IP9_$uXeZRZbBf5K3bv!tqUc)@<^B1UM4cvvmF|F{NXC_Dq3Gv&CR|cJx zfBDoB3}O+6?O2kAq+-8Sex0H!=+fQ)7M!6y>n`!>-X9#fhO6$sRxu$nebNwZ34gMW(v=ee; zu4%A!Qjyuze7WQ8GjS%CBjfDx@SUm=k`Aptj>?Y9vYI`rd*Gr4(EX*o|{mjo~}!R-5!RhGv-hUs-)=D7ride`Q55;K?nrrO-(jKSoTklPNB&ZrN($h zv`Rvlm+p5^G}&tMF17_vnki}k%>MnjRJ8DtWQGEeo*i(TJnK;d$;Vfe!?IwSr%g4O zPVx!B7acAj`TmzG(V!$E+zj>(_6$AayMBp0zo#jGViRo`XEf_od437EEjt?76SeWFPdK zSH8L~BS$tvo-n!UG8BOEJ`1&uOyuo=tpAP7Sse+V5E-h z6TIs?EB-TeqU~JlH@HK%p0M}MMAc|pM|d63g~qv+s4%`@QLzW%NWD2>-~#1Gls!=c zu5CGhcLIpl(N;+p_6V_hu_FfLqamI&YaF37U51+9MexT@yym2zz-tqK5dkGR!|~3c zbrf8c$5TExtu}Gwe&O4|N()SPE?BX^Y~3qn=f>BUDMc992}i4ABRQ3Kktj0z{#RqH zIVpC%BEqbUyymuEYWU?iZ;w7*{g&h7_58_F?6(40=o*;p5p21?$$44`8oL%R*|`h` zLJb}n#2&QW1_e6s?}Vt4+U;{(PO0$H26ykXJyHKr%B}zLm#bD5#knwWf4BaSnZZXK zBUy0u3RPPIgI;oiPJB)pu`=8ilFwj!oQCP$rMi1S{5EE+bA0i$-cKU~1w$GQSgCy5 z|1tI6fmHwR`?!@o&M}Y8IcDe>;mGFTkP#_S*_%Qrdyiu~qLffNgk)r-vUdrQkcO2R z*?avS&)56)`h0)qKYtvL^L#$meP8!=T{nqtY1h@ZsPrDOz?9th z#}m2kB3j+}|0u6V8Gi02#&H5`=Jv>NVV~=D>}7VUl_YYi zN63IKnN*)~!IwJKkLWIt`f}WY{UI4oY(lJbtYUOO-qGOisP8!Hx&d79qIWcTUVuJb zwW#|#N*I0Fx_}k`#DM-0GKZ+00(0u)D4G0Kma-(o^;*X?yC!GP%AIH70z0y)-Wg9# z);{{a&-oJd+UqF;LUj3k_C<^<-Ht;ImG3>NZyE(B!ZR^DwtdN){JyjmRnC8lLKE;k zIB64ErVTEqtRSD8LPLKl>$;6Qn^J1o{+t7qZ3~E1a4yK`D67j>CdM}Y(x)zpXKbis z-3OjM5wS9Krf~|>S*J5D$szD#&39Ll8zsfuwCOXjQlB3n_M}s*r~X`NeZz$h6HMIv zsL5S5*a=IVN>iAgK)iMM(mSy3W&M1jO6IN%h^&IV1CXyDqbSOD=!VpnCPAT z3v&m5Dh+LJKKb+Jz_Nz4>-u=4Jn2fiW=>3fXS3VIJO`ELfc`J%YB<5$Opqs|I{o`b zfeUMYt7j}hhUE|TfTog<_PP3>mNN5?QEDw*gcG zt6Ue2y;pMdH9FZh(#CtI2#G`?(w*fS0xt{J}yADTuqWbi`LHEMw(GykN<~^cYc_uurE}?FkljBc>XUn)N zOq2LqEdB&GSYAb0V1i}!Qs1F#x=dX++rcHRZoGHW5Px^Edj|&p@FJK|&t*jdxe_mf z-{bbEHtLoxP?L^kY9u*oZ$%M%`RDmthJ0dlYbSS!L+N;EljaNEHK}l`ZjucB< zSG>X@4*vSzhp12>L{WiBWO2fk2E*OdF}CSb>BTlYgJM;JL#t3dzXv86(?*v{&EH_A z^M7BeEV*>b!%b%?;9dIW$phSc3r%#-1g`n^a0NX?dPHWl(4}5DZ`#X*#tbnh;Q#l* z-Lk#@$Uad#<8w?;OlT)F4w)P2gJ0wBM_jP)c?zgwNF)urW|flWmi*7^|Gu+wN!J4q zdc+$g$-V$KnO_;lq5#EvX_2aX89}-0#d{Lb5l}sY7z?Wn(~s6#Ndl1JwF-0i|2_n+ z)}pAM7+=*8fHzziN%BJg!s17KvCT@Ztk^$KcWD@1DKqt%&iLPFb9BU??I@3OP*pz# zK7)p)&B!4oM3O^Svc9^UNbS9k0)U)@l%1S>#6>6AFtKL%_5Z#}RPZj%2L`S2OGm~} zFfxau%_mTp$>Yr(A+BJauVl7}VRm|l4P2a2WfFgoQiJupbc6c8`A??5%hqAzo@j5ezt+x_ za*lP843Wd*c;U%;w~QGxO6D(W7cJU4K2b105+aZD1-RBq8O=T6Hef)6TKR85;OUtb z9ZNR3-JC>?pJZc2;ER%Wo5_ap7$-bEU{?%f2^@7%r0l&E6E>5y0k&)L?#HrSu<-wu zu44e~p>7ISmZyXUr_{<>CZx6j}g-`4x@dYlX#Vg86z4hd~?)h zWdHv^1wGS_!SaJ)mRXnAY-Z)g*b7XBE68H{>!MCVU)dZlnI2`8M}K&fDK7aGztAWNN%%f-^N#z^?)2zF0@h&yw zql<`J0XWSO-lf@hK8j!7LOakgVTJ&>`|oD*uPjXFlymO6y$|F+BJn{eI-Ltl3Im_n zDg$NJ`uxPAlcy{E*ktspvlDeP@vKy)+eDF%2ByuJYybWSJkB4##jAYR^Yu$CjTB?7 z4dHKujDghctuwdspf~AM8G%$KhovkCNty9ThT)7lB=WpK5Fsm1`0qiaV~VjY23PMC z>Fvp1e}>y_L8MqUkE`ggh8OnsBsGr+vWc@V9nJl*uC)AjL&6CkCpFSkod6~Gz%bY5 zqHBU56No-P-7M<6>_`o+Fs^JOJlqmDr4%2Zif7f9j7mOW`}eygDUbwDya4u|cIRW7 z+5yf^pV*6+Deb%bTV3qLV6=szFY2sBVaVMKc5y~zm`{0+0iU<-$bWClJf8J+=0M_w zX6zAH8FJ`mx=!Xo2$}U~)|$Y?BW^G>ZT6E*@q9~MRWUPRuC*#k=Tbn4*+WVb%zwXq zUuEp<^M15CN(^GCkLRp-B5xe=WqZU;csM>LCZxosrUFJDpYu(gl$IZ z7Clzv>`CHz6VGb}4;1oB16u#vhCTDGak3yM7ZMFxzE5r7e2HaIjXFRuoBmL=)zOs} zxxt?d)-Va0*VZ&vG*>*o@H#d6H85!jDi+y3Ty3_w#|1C&(&vbIP96eS=!-nBmj8H~ z*WWEyR+*}2q?iOno(kuREB)=eOcfUFmx1*7Zg2T}8w8-IyH{U}8Ja7?XIyP@rkbH} z$emxH#px#T%b42GsfH-EEzhb+%6p0jNo}{(X_JM<>7^XG4yX%k1@| zzxw~2mP?VUY@j3a^DwbsiIq&`&ogxj%hd4gB!>2&AIN;2b&CteYO`_$nQrDp+#G=B zJ@Z3%^xH^&JqE>I!yRClHoWyqhuu=>Iw<4%aXWs)9bI3zU?vu239jLcC0h7^D-3DWdP5|5qC<822ee=}w z8dTNyGgLp`aB5^RFt=$9h_qWLfQI9O zB_zZm=D*oSGw2?j3(NyjhGhVu7>p`{K;21?FVKnzkRaf`b}OVvha?o-2S0%eNET$TRC-|jcN7@a zy;%k&-Cn2;TBC8!c(MQ1?7er&KT-{VdMR&D7;Nr9s(q=Zy=m>xFbqUc zUk2Ldujykw0Z=%w>qz^w&g)%_2y*(aR;<@N{X-@&6ZG07FzThw}Qf8$Qqu69rIu93IuKtbh0@nhCtRGOI*|fZAlDWilqqP1Nx)fjgC+py;ywf? z>Z{Ps$Ee^h9 zg{4!LCxFKIiaJlo$tFQzM_%aUPB?5Z&`G8lh+iy1S0Qo4D6ghd#NYaT%pzf*%#{u1{@P!kIoxcM@LlsD6roO58Nx%Ike_rGd z{)0iN77D44*c4?x@izkx^C7UBdUxf^9UeJ7(4QfBkpN2371Q?)faX4fH~eV*!@7Wb z^g|oEnOWes-xmhfWsyREd;Q$*?54KxUdUe`>!VpNh*~D}#A*)~vCI)k;csuvFv_u2O3L!bmK-X*+qz-qX=EqJ|t>pvX zk$8#(unEyLFYw9u^MWM4jL2!o4f40V!GRgYQ{}()D4Jkw?j1%7iH@I;5rDoVDE!gI z_qRHuC`4i!0R;DYsVI8YjE^(Hy9Rjr7s_mJeKC3AyY|r@-*X8$3+t{oj=@s2z*b#F zJD9Ns{YJz5rpZ_obBf~p6JKWlLEk($X&^&d)UsC997BctaWvD)4lw8wJvOYFg?w5D zj!u9ggVzAfTfn+vc2UTUx?&c|d&)ruk5VSof7Q?k2I@Y&qq02}Y#AU4%GW9p$_D2G zXi>(3%R_SzLom^`t5pC^(p`J?-QPPKNQ;wsl^VrVni>au|%jqg!7# zJf=?T?_p`@nb}hw3fiZb=`*^ zn?H%K3jo0VKcvTmoc%w}KYS)xTe8pH3u-B`#dr-KH3~-M`2M!zkvqo@Sh+&3a&>Ox zJg~OwD!NgiOOn*XSJPjxqCKs)^y$SEH}8=iNVLF~$hil5U@YGqU~faLbNa>-`m#U)5Wwcg2u-(YR6vJBKFYF58wc;{@@R z7~YUz@SrnGoQsHq$>R;RVVW_h$M9B?DcZ>A%+${zk=ErPTVyA!W~1OYKk%Ltl1(>Y zHI41j8v2rP@PPHXrTAk;I}T0y_4KWVStJTcXZi}8bw?HNMIAXyCZ+A^69)^_d+hhq~C@O^~`-IdA0!3lK}whC4ExagJd@Pt4%7t& zK8axw@XnyTz;sMX*aQt6-1dS6mXOlE3|oy>PtgzP`a@g}RHo5nQy?-?I`7O)8lUVA z0AXK1bSm19se%;$BAOSN+Aho#nYp`~aiWr$kCUS*^I7242;nxn7<}++rW@d-cWYZNaZ8QXIpOS8bt!LKjINvPN!tXb&XP(@=}kGVE(Nao(Kh0~WQ>f&Pv_F; z%P9wr3m;gWrD<;1jUZ>l&7jcUdhgtZB4<>pMyxyvJ*K(7EkN+L{_jqi$U6IzkInh3 z7xSMnz~1?I+o_HE+WZr>ZA^M6G2k0TqtJ)8EBBE<>F$~=lP#}S#V!;Jnmr=4zrb4I zh;GT516)ppIGAm<8HG6fHlAYZjk~y` z+fB8$D&Z4<^APQIg-R*5RUp@3KgNPD_#tlb>1uWu60ST}CSYKZXe})N7wTlvVHYN% zrKB;O^tg!nY@nu?#trRs5uY-T+7qc}Cw#MTh%yR0j`IEb!G~bU9=NSQ#^w?fg)}?`y~Hy0VD$Fo8yS^ed7cSW<%>Tj>i9|j3Q zl}dtmro*^+~Fw2&UmrWCF;}ATVOV}^vBCa~8A`g`EiPGf09AAZ-Yy9My}G40A{_7$Cc_5zP^ z{)UH=ywM34l6w%2Y~h7CTx*iIz8`X$@;FIMX-SP)6{_Zjp9>WyBSYT4n&{4Rj@N5g z_9tL-XS95rK&7S!2`weZNh|PBOF*?>=3je<&VQ^?F_*&cd$_P@A6PC~XQaLf0g4Bd z?ZZtVe<}18*h;^{PRM30on8A`n$Q)EZJL0U>;st))8Q^ij~6bzavXnmc~SuWjl&!3 z+HpNYjlJ#q%c14;A;fwn?#cm5O9;O5lhIexvCh>|PHRiGi~3i7v*ITk6=lwuuS26$ zTWDlZiF&fbMU8N$@|NOOp7T66lr3R6{{hu=$YajY{`rk%(X)>NAF(d)zxzVU$-N=} zqS`Fiw+kK`WynArnd6f9$}8GV7XJ@kfQz^+6KwA(v;4jCVdXtD9P^@eWvp#4e*DMR zCsR7VC!o+oLmE$gF8M=|IiWz{BSR}oWW>VFg0O0{lzyoASNP&4kgQ&|8fhqw($*rX z7n!8~UP*{BwT{jl(7lx8n_S~Fx%sj%ma#x#$J;Fno?>}a=`$s#uWxkAGA8REz9>mX z(Icy7f_>xoE{Z|I-yU97a(c1P$Q_S-z%tb<&$N4Tl_jZh3UMw&dNRx+2|@4rk}xOI zzzAP-WmR7~=c>Ubu&w43#T{5?RzWV&8*z}_El#t8~$6HDbRHnRCY}PX;6kCdUxm$8i7~-OL zb>GjelFn1F-ZiUFqcH6;^{pS5_dS{VD>n*fBukb!pxb1gU=ig)zK5JJ&Z9Rik3wtJ zTK(^{=RBSvra>JWzdSL{L=S$(_sHnItpmh2IGmueY%|6ZyVl!8qh|(zJaLmb-WiGp zHja(}ra@udt?0)L$AXq{gfhT3{st&o*#}=C`kE1o3Un;DPiyZp8W=~87NL2z6?r2R^kU?U!i*R zYlxNGv0!juxRrC8`7vy_STUz!`*Bu*Km3QnuYj4%gNMc2fE!RZtpwX#P~R{1X_6?A zIbH<*y3R|!_-|V;@4&)J;%_Y8Bb~XwHAQSMulZJFq@N!K6!?W(jiB|zy$nPU;7!*` zj@Prh>Mc3U)1cZw{+_iYw)rzV`s`Q6?#~*p){3v(&l*oE{$Zr>O(?|@>o_OYPdH7s zI^=dhh_asK`TXcnawnyVDE7HQGafnK`l)%nL6mD3@@a(l?4BU}9Dstlt#^PyR8&SA z;x)&)bSyiNIpNVY+poO(WKP%2YNj~qKFI`!D4VQmdR4?p-?hI*Rlqy}d~zdIIV>Od zoa-D|z9NQpNI-y@B%xRq9g`i~WqSC^6nl~aQpK3!l*d^yMaUssqGMrG?1B~rx)K^vx*Bcz) z>UKOQ%+Z_8qze7^ZEU}_mi;^~sqXJxTyWT!U3}qlX8aD`5lT=mtCvs=P8>CHN!Bf* zlEo?N3_f?Y*ZN{p81FSBoJu}Zxkl+)isd4X>AoSv8$0lRA z+t9&6JkD%kI`6oUEZm|L$JUDKo&HgYFwIUD*2q&9%dU{T<;FHmZIfZ!>i3-JBqOHpfDtYYx2 ztfgY))B8J*#Poi32eSikX87(u>B$YOt0413*T$=Qqw6=}WqJlVhFCjHwt4iXK>3yE z6am6uBkickuw$6%r+TpJl+0|j>eYcAmfFL2DopmAx6<=pz(;(@_CWrhA*NUQ(;ewK z+$E9`g7H>EdvkRK2W11;&xDKirQSp8l~4ajcXhr&g zmj}b^@LdCEfBNB0fD{Z$roN)FhvaqT zJK&EJfjPxpgH6+GT_NIcpI>5WmG3uYrPik&`PyK*Z;a)dHir|cW|5MnkFZT})oy1h zQg49q3;;Z(TIg5C1%IHBDh^@@Xx|7+1uU1sx|w4lqe!2E_%(RARU?UZGIme+Y#;`* z7lC(9K%~Gi^V;njEGvBQvY%v8JS#CX8XD$jqr!}vK-_wf@RM}<52UFr zFt7n3O{PJ^xY9yA)`NYpKZJ6Z?^!zD*bMdX#Vs#U%tHZ@2km<<4TB2psuSL00!A{o zo@8~&*v|^a^JE;HD?r~GP1e@IexIAKtJh8DJvwLBsYtG+uwp88@SeS%?|_qPlXnwy z><8>Jj8Y{&$rDQlcIQ<>M7z#Y*{?y^$DG2iG=b!5lKJxAzoC_mL>@B)Zv!3;QPqsP z8FT)+oH_A*t3nbm^jt}{@HkE!P6h9qm)#1HBXc1wlhK^WcdZ9?f;1*0GZhw8=wL)H z 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 z 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 { + await invoke('plugin:process|exit', { code }) +} + +/** + * Exits the current instance of the app then relaunches it. + * @example + * ```typescript + * import { relaunch } from '@tauri-apps/plugin-process'; + * await relaunch(); + * ``` + * + * @returns A promise indicating the success or failure of the operation. + * + * @since 2.0.0 + */ +async function relaunch(): Promise { + await invoke('plugin:process|restart') +} + +export { exit, relaunch } diff --git a/plugins/process/package.json b/plugins/process/package.json new file mode 100644 index 0000000..2bde9de --- /dev/null +++ b/plugins/process/package.json @@ -0,0 +1,29 @@ +{ + "name": "@tauri-apps/plugin-process", + "version": "2.3.1", + "license": "MIT OR Apache-2.0", + "authors": [ + "Tauri Programme within The Commons Conservancy" + ], + "repository": "https://github.com/tauri-apps/plugins-workspace", + "type": "module", + "types": "./dist-js/index.d.ts", + "main": "./dist-js/index.cjs", + "module": "./dist-js/index.js", + "exports": { + "types": "./dist-js/index.d.ts", + "import": "./dist-js/index.js", + "require": "./dist-js/index.cjs" + }, + "scripts": { + "build": "rollup -c" + }, + "files": [ + "dist-js", + "README.md", + "LICENSE" + ], + "dependencies": { + "@tauri-apps/api": "^2.11.0" + } +} diff --git a/plugins/process/permissions/autogenerated/commands/exit.toml b/plugins/process/permissions/autogenerated/commands/exit.toml new file mode 100644 index 0000000..8abaf29 --- /dev/null +++ b/plugins/process/permissions/autogenerated/commands/exit.toml @@ -0,0 +1,13 @@ +# Automatically generated - DO NOT EDIT! + +"$schema" = "../../schemas/schema.json" + +[[permission]] +identifier = "allow-exit" +description = "Enables the exit command without any pre-configured scope." +commands.allow = ["exit"] + +[[permission]] +identifier = "deny-exit" +description = "Denies the exit command without any pre-configured scope." +commands.deny = ["exit"] diff --git a/plugins/process/permissions/autogenerated/commands/restart.toml b/plugins/process/permissions/autogenerated/commands/restart.toml new file mode 100644 index 0000000..63b228c --- /dev/null +++ b/plugins/process/permissions/autogenerated/commands/restart.toml @@ -0,0 +1,13 @@ +# Automatically generated - DO NOT EDIT! + +"$schema" = "../../schemas/schema.json" + +[[permission]] +identifier = "allow-restart" +description = "Enables the restart command without any pre-configured scope." +commands.allow = ["restart"] + +[[permission]] +identifier = "deny-restart" +description = "Denies the restart command without any pre-configured scope." +commands.deny = ["restart"] diff --git a/plugins/process/permissions/autogenerated/reference.md b/plugins/process/permissions/autogenerated/reference.md new file mode 100644 index 0000000..45d74b7 --- /dev/null +++ b/plugins/process/permissions/autogenerated/reference.md @@ -0,0 +1,76 @@ +## Default Permission + +This permission set configures which +process features are by default exposed. + +#### Granted Permissions + +This enables to quit via `allow-exit` and restart via `allow-restart` +the application. + +#### This default permission set includes the following: + +- `allow-exit` +- `allow-restart` + +## Permission Table + + + + + + + + + + + + + + + + + + + + + + + + + + + +
IdentifierDescription
+ +`process:allow-exit` + + + +Enables the exit command without any pre-configured scope. + +
+ +`process:deny-exit` + + + +Denies the exit command without any pre-configured scope. + +
+ +`process:allow-restart` + + + +Enables the restart command without any pre-configured scope. + +
+ +`process:deny-restart` + + + +Denies the restart command without any pre-configured scope. + +
diff --git a/plugins/process/permissions/default.toml b/plugins/process/permissions/default.toml new file mode 100644 index 0000000..69a9b00 --- /dev/null +++ b/plugins/process/permissions/default.toml @@ -0,0 +1,14 @@ +"$schema" = "schemas/schema.json" + +[default] +description = """ +This permission set configures which +process features are by default exposed. + +#### Granted Permissions + +This enables to quit via `allow-exit` and restart via `allow-restart` +the application. +""" + +permissions = ["allow-exit", "allow-restart"] diff --git a/plugins/process/permissions/schemas/schema.json b/plugins/process/permissions/schemas/schema.json new file mode 100644 index 0000000..9d68fc6 --- /dev/null +++ b/plugins/process/permissions/schemas/schema.json @@ -0,0 +1,330 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "PermissionFile", + "description": "Permission file that can define a default permission, a set of permissions or a list of inlined permissions.", + "type": "object", + "properties": { + "default": { + "description": "The default permission set for the plugin", + "anyOf": [ + { + "$ref": "#/definitions/DefaultPermission" + }, + { + "type": "null" + } + ] + }, + "set": { + "description": "A list of permissions sets defined", + "type": "array", + "items": { + "$ref": "#/definitions/PermissionSet" + } + }, + "permission": { + "description": "A list of inlined permissions", + "default": [], + "type": "array", + "items": { + "$ref": "#/definitions/Permission" + } + } + }, + "definitions": { + "DefaultPermission": { + "description": "The default permission set of the plugin.\n\nWorks similarly to a permission with the \"default\" identifier.", + "type": "object", + "required": [ + "permissions" + ], + "properties": { + "version": { + "description": "The version of the permission.", + "type": [ + "integer", + "null" + ], + "format": "uint64", + "minimum": 1.0 + }, + "description": { + "description": "Human-readable description of what the permission does. Tauri convention is to use `

` headings in markdown content for Tauri documentation generation purposes.", + "type": [ + "string", + "null" + ] + }, + "permissions": { + "description": "All permissions this set contains.", + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "PermissionSet": { + "description": "A set of direct permissions grouped together under a new name.", + "type": "object", + "required": [ + "description", + "identifier", + "permissions" + ], + "properties": { + "identifier": { + "description": "A unique identifier for the permission.", + "type": "string" + }, + "description": { + "description": "Human-readable description of what the permission does.", + "type": "string" + }, + "permissions": { + "description": "All permissions this set contains.", + "type": "array", + "items": { + "$ref": "#/definitions/PermissionKind" + } + } + } + }, + "Permission": { + "description": "Descriptions of explicit privileges of commands.\n\nIt can enable commands to be accessible in the frontend of the application.\n\nIf the scope is defined it can be used to fine grain control the access of individual or multiple commands.", + "type": "object", + "required": [ + "identifier" + ], + "properties": { + "version": { + "description": "The version of the permission.", + "type": [ + "integer", + "null" + ], + "format": "uint64", + "minimum": 1.0 + }, + "identifier": { + "description": "A unique identifier for the permission.", + "type": "string" + }, + "description": { + "description": "Human-readable description of what the permission does. Tauri internal convention is to use `

` headings in markdown content for Tauri documentation generation purposes.", + "type": [ + "string", + "null" + ] + }, + "commands": { + "description": "Allowed or denied commands when using this permission.", + "default": { + "allow": [], + "deny": [] + }, + "allOf": [ + { + "$ref": "#/definitions/Commands" + } + ] + }, + "scope": { + "description": "Allowed or denied scoped when using this permission.", + "allOf": [ + { + "$ref": "#/definitions/Scopes" + } + ] + }, + "platforms": { + "description": "Target platforms this permission applies. By default all platforms are affected by this permission.", + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/Target" + } + } + } + }, + "Commands": { + "description": "Allowed and denied commands inside a permission.\n\nIf two commands clash inside of `allow` and `deny`, it should be denied by default.", + "type": "object", + "properties": { + "allow": { + "description": "Allowed command.", + "default": [], + "type": "array", + "items": { + "type": "string" + } + }, + "deny": { + "description": "Denied command, which takes priority.", + "default": [], + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "Scopes": { + "description": "An argument for fine grained behavior control of Tauri commands.\n\nIt can be of any serde serializable type and is used to allow or prevent certain actions inside a Tauri command. The configured scope is passed to the command and will be enforced by the command implementation.\n\n## Example\n\n```json { \"allow\": [{ \"path\": \"$HOME/**\" }], \"deny\": [{ \"path\": \"$HOME/secret.txt\" }] } ```", + "type": "object", + "properties": { + "allow": { + "description": "Data that defines what is allowed by the scope.", + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/Value" + } + }, + "deny": { + "description": "Data that defines what is denied by the scope. This should be prioritized by validation logic.", + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/Value" + } + } + } + }, + "Value": { + "description": "All supported ACL values.", + "anyOf": [ + { + "description": "Represents a null JSON value.", + "type": "null" + }, + { + "description": "Represents a [`bool`].", + "type": "boolean" + }, + { + "description": "Represents a valid ACL [`Number`].", + "allOf": [ + { + "$ref": "#/definitions/Number" + } + ] + }, + { + "description": "Represents a [`String`].", + "type": "string" + }, + { + "description": "Represents a list of other [`Value`]s.", + "type": "array", + "items": { + "$ref": "#/definitions/Value" + } + }, + { + "description": "Represents a map of [`String`] keys to [`Value`]s.", + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/Value" + } + } + ] + }, + "Number": { + "description": "A valid ACL number.", + "anyOf": [ + { + "description": "Represents an [`i64`].", + "type": "integer", + "format": "int64" + }, + { + "description": "Represents a [`f64`].", + "type": "number", + "format": "double" + } + ] + }, + "Target": { + "description": "Platform target.", + "oneOf": [ + { + "description": "MacOS.", + "type": "string", + "enum": [ + "macOS" + ] + }, + { + "description": "Windows.", + "type": "string", + "enum": [ + "windows" + ] + }, + { + "description": "Linux.", + "type": "string", + "enum": [ + "linux" + ] + }, + { + "description": "Android.", + "type": "string", + "enum": [ + "android" + ] + }, + { + "description": "iOS.", + "type": "string", + "enum": [ + "iOS" + ] + } + ] + }, + "PermissionKind": { + "type": "string", + "oneOf": [ + { + "description": "Enables the exit command without any pre-configured scope.", + "type": "string", + "const": "allow-exit", + "markdownDescription": "Enables the exit command without any pre-configured scope." + }, + { + "description": "Denies the exit command without any pre-configured scope.", + "type": "string", + "const": "deny-exit", + "markdownDescription": "Denies the exit command without any pre-configured scope." + }, + { + "description": "Enables the restart command without any pre-configured scope.", + "type": "string", + "const": "allow-restart", + "markdownDescription": "Enables the restart command without any pre-configured scope." + }, + { + "description": "Denies the restart command without any pre-configured scope.", + "type": "string", + "const": "deny-restart", + "markdownDescription": "Denies the restart command without any pre-configured scope." + }, + { + "description": "This permission set configures which\nprocess features are by default exposed.\n\n#### Granted Permissions\n\nThis enables to quit via `allow-exit` and restart via `allow-restart`\nthe application.\n\n#### This default permission set includes:\n\n- `allow-exit`\n- `allow-restart`", + "type": "string", + "const": "default", + "markdownDescription": "This permission set configures which\nprocess features are by default exposed.\n\n#### Granted Permissions\n\nThis enables to quit via `allow-exit` and restart via `allow-restart`\nthe application.\n\n#### This default permission set includes:\n\n- `allow-exit`\n- `allow-restart`" + } + ] + } + } +} \ No newline at end of file diff --git a/plugins/process/rollup.config.js b/plugins/process/rollup.config.js new file mode 100644 index 0000000..1f349ec --- /dev/null +++ b/plugins/process/rollup.config.js @@ -0,0 +1,7 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +import { createConfig } from '../../shared/rollup.config.js' + +export default createConfig() diff --git a/plugins/process/src/commands.rs b/plugins/process/src/commands.rs new file mode 100644 index 0000000..3777294 --- /dev/null +++ b/plugins/process/src/commands.rs @@ -0,0 +1,15 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use tauri::{AppHandle, Runtime}; + +#[tauri::command] +pub fn exit(app: AppHandle, code: i32) { + app.exit(code) +} + +#[tauri::command] +pub fn restart(app: AppHandle) { + app.request_restart() +} diff --git a/plugins/process/src/lib.rs b/plugins/process/src/lib.rs new file mode 100644 index 0000000..d7f05f4 --- /dev/null +++ b/plugins/process/src/lib.rs @@ -0,0 +1,24 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +//! This plugin provides APIs to access the current process. To spawn child processes, see the [`shell`](https://github.com/tauri-apps/tauri-plugin-shell) plugin. + +#![doc( + html_logo_url = "https://github.com/tauri-apps/tauri/raw/dev/app-icon.png", + html_favicon_url = "https://github.com/tauri-apps/tauri/raw/dev/app-icon.png" +)] + +use tauri::{ + plugin::{Builder, TauriPlugin}, + Runtime, +}; + +mod commands; + +/// Initializes the plugin, exposing commands to exit or restart the current process. +pub fn init() -> TauriPlugin { + Builder::new("process") + .invoke_handler(tauri::generate_handler![commands::exit, commands::restart]) + .build() +} diff --git a/plugins/process/tsconfig.json b/plugins/process/tsconfig.json new file mode 100644 index 0000000..4374468 --- /dev/null +++ b/plugins/process/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "rootDir": "guest-js" + }, + "include": ["guest-js/*.ts"] +} diff --git a/plugins/shell/CHANGELOG.md b/plugins/shell/CHANGELOG.md new file mode 100644 index 0000000..e8657a1 --- /dev/null +++ b/plugins/shell/CHANGELOG.md @@ -0,0 +1,159 @@ +# Changelog + +## [2.3.6] + +- [`db9c5998`](https://github.com/tauri-apps/plugins-workspace/commit/db9c5998feff9384f9cbbefcbe0d45937c00a1fc) ([#3531](https://github.com/tauri-apps/plugins-workspace/pull/3531) by [@Legend-Master](https://github.com/tauri-apps/plugins-workspace/../../Legend-Master)) Fix missing `consumer-rules.pro` on Gradle v9. + +## \[2.3.5] + +### bug + +- [`d4613ff0`](https://github.com/tauri-apps/plugins-workspace/commit/d4613ff0029ea65f5c6895c3a6f8f5668dd1b55e) ([#3234](https://github.com/tauri-apps/plugins-workspace/pull/3234)) Make sidecars work in tests. Executable resolution is aware of the "deps" directory. + +## \[2.3.4] + +- [`31415eff`](https://github.com/tauri-apps/plugins-workspace/commit/31415effdf5a9ced19934a681cb044a732174088) ([#3183](https://github.com/tauri-apps/plugins-workspace/pull/3183) by [@Tunglies](https://github.com/tauri-apps/plugins-workspace/../../Tunglies)) Docs on example to Encoding usage in `Command::spawn`. No user facing changes. + +## \[2.3.3] + +- [`93426f85`](https://github.com/tauri-apps/plugins-workspace/commit/93426f85120f49beb9f40222bff45185a32d54a9) Fixed an issue that caused docs.rs builds to fail. No user facing changes. + +## \[2.3.2] + +- [`6c9b61fb`](https://github.com/tauri-apps/plugins-workspace/commit/6c9b61fb658145d13893626112fc489f7458aa17) ([#3039](https://github.com/tauri-apps/plugins-workspace/pull/3039) by [@FabianLars](https://github.com/tauri-apps/plugins-workspace/../../FabianLars)) On Android, updated compileSdk to 36. + +## \[2.3.1] + +- [`d865ed47`](https://github.com/tauri-apps/plugins-workspace/commit/d865ed47685c3923e894f7d10ee4c037507037e6) ([#2950](https://github.com/tauri-apps/plugins-workspace/pull/2950) by [@lucasfernog](https://github.com/tauri-apps/plugins-workspace/../../lucasfernog)) Fix sidecar with dots in the filename not working on Windows. + +## \[2.3.0] + +- [`f209b2f2`](https://github.com/tauri-apps/plugins-workspace/commit/f209b2f23cb29133c97ad5961fb46ef794dbe063) ([#2804](https://github.com/tauri-apps/plugins-workspace/pull/2804) by [@renovate](https://github.com/tauri-apps/plugins-workspace/../../renovate)) Updated tauri to 2.6 + +## \[2.2.2] + +- [`f634e524`](https://github.com/tauri-apps/plugins-workspace/commit/f634e5248ebe428f8305a59f74c13fc15147fb8e) This is an "empty" release to update the plugins' source files on crates.io and docs.rs. This should fix docs.rs build failures for projects using tauri plugins as dependencies. + +## \[2.2.1] + +### bug + +- [`9cf0390a`](https://github.com/tauri-apps/plugins-workspace/commit/9cf0390a52497e273db1a1b613a0e26827aa327c) Apply the default open validation regex `^((mailto:\w+)|(tel:\w+)|(https?://\w+)).+` when the open configuration is not set, preventing unchecked input from being used in this scenario (previously the plugin would skip validation when it should disable all calls). This keeps backwards compatibility while still fixing this vulnerability. + The scope is no longer validated for Rust calls via `ShellExt::shell()` so if you need to block JavaScript from calling the API you can simply set `tauri.conf.json > plugins > shell > open` to `false`. + +## \[2.2.0] + +- [`3a79266b`](https://github.com/tauri-apps/plugins-workspace/commit/3a79266b8cf96a55b1ae6339d725567d45a44b1d) ([#2173](https://github.com/tauri-apps/plugins-workspace/pull/2173) by [@FabianLars](https://github.com/tauri-apps/plugins-workspace/../../FabianLars)) Bumped all plugins to `v2.2.0`. From now, the versions for the Rust and JavaScript packages of each plugin will be in sync with each other. + +## \[2.0.1] + +- [`51ddf6a7`](https://github.com/tauri-apps/plugins-workspace/commit/51ddf6a71544acfb261ffc9393dab1342da0a219) ([#1881](https://github.com/tauri-apps/plugins-workspace/pull/1881) by [@amrbashir](https://github.com/tauri-apps/plugins-workspace/../../amrbashir)) On Windows, Fix `open` JS API hanging and freezing the app. + +## \[2.0.1] + +- [`a1a82208`](https://github.com/tauri-apps/plugins-workspace/commit/a1a82208ed4ab87f83310be0dc95428aec9ab241) ([#1873](https://github.com/tauri-apps/plugins-workspace/pull/1873) by [@lucasfernog](https://github.com/tauri-apps/plugins-workspace/../../lucasfernog)) Downgrade MSRV to 1.77.2 to support Windows 7. + +## \[2.0.0] + +- [`e2c4dfb6`](https://github.com/tauri-apps/plugins-workspace/commit/e2c4dfb6af43e5dd8d9ceba232c315f5febd55c1) Update to tauri v2 stable release. + +## \[2.0.0-rc.4] + +- [`44273b98`](https://github.com/tauri-apps/plugins-workspace/commit/44273b988957a254eff715d6be7547d2ace882e1) ([#1839](https://github.com/tauri-apps/plugins-workspace/pull/1839) by [@amrbashir](https://github.com/tauri-apps/plugins-workspace/../../amrbashir)) Fix the plugin schema requiring to set `sidecar` property when it is in fact optional. + +## \[2.0.0-rc.1] + +- [`e2e97db5`](https://github.com/tauri-apps/plugins-workspace/commit/e2e97db51983267f5be84d4f6f0278d58834d1f5) ([#1701](https://github.com/tauri-apps/plugins-workspace/pull/1701) by [@lucasfernog](https://github.com/tauri-apps/plugins-workspace/../../lucasfernog)) Update to tauri 2.0.0-rc.8 + +## \[2.0.0-rc.2] + +- [`b9147758`](https://github.com/tauri-apps/plugins-workspace/commit/b914775898c2bee7ceb20bd17ee595005cd17a64) ([#1679](https://github.com/tauri-apps/plugins-workspace/pull/1679) by [@lucasfernog](https://github.com/tauri-apps/plugins-workspace/../../lucasfernog)) Explicitly set a minimum macOS version for the Swift package. + +## \[2.0.0-rc.1] + +### changes + +- [`6b079cfd`](https://github.com/tauri-apps/plugins-workspace/commit/6b079cfdd107c94abc2c7300f6af00bac3ff4040) ([#1649](https://github.com/tauri-apps/plugins-workspace/pull/1649) by [@ahqsoftwares](https://github.com/tauri-apps/plugins-workspace/../../ahqsoftwares)) Remove targetSdk from build.kts files as it is deprecated and will be removed from DSL v9.0 + +## \[2.0.0-rc.0] + +- [`9887d1`](https://github.com/tauri-apps/plugins-workspace/commit/9887d14bd0e971c4c0f5c1188fc4005d3fc2e29e) Update to tauri RC. +- [`34df132f`](https://github.com/tauri-apps/plugins-workspace/commit/34df132fb14212ba7330adc9ccd64267751950c8) ([#1603](https://github.com/tauri-apps/plugins-workspace/pull/1603)) Change the `open` scope validator regex to match on the entire string. +- [`34df132f`](https://github.com/tauri-apps/plugins-workspace/commit/34df132fb14212ba7330adc9ccd64267751950c8) ([#1603](https://github.com/tauri-apps/plugins-workspace/pull/1603)) Change the `execute` scope argument validator regex to match on the entire string by default. + If this behavior is not desired check the `raw` boolean configuration option that is available along the `validator` string. + +## \[2.0.0-beta.9] + +- [`99d6ac0f`](https://github.com/tauri-apps/plugins-workspace/commit/99d6ac0f9506a6a4a1aa59c728157190a7441af6) ([#1606](https://github.com/tauri-apps/plugins-workspace/pull/1606) by [@FabianLars](https://github.com/tauri-apps/plugins-workspace/../../FabianLars)) The JS packages now specify the *minimum* `@tauri-apps/api` version instead of a single exact version. +- [`6de87966`](https://github.com/tauri-apps/plugins-workspace/commit/6de87966ecc00ad9d91c25be452f1f46bd2b7e1f) ([#1597](https://github.com/tauri-apps/plugins-workspace/pull/1597) by [@Legend-Master](https://github.com/tauri-apps/plugins-workspace/../../Legend-Master)) Update to tauri beta.25. + +## \[2.0.0-beta.8] + +- [`22a17980`](https://github.com/tauri-apps/plugins-workspace/commit/22a17980ff4f6f8c40adb1b8f4ffc6dae2fe7e30) ([#1537](https://github.com/tauri-apps/plugins-workspace/pull/1537) by [@lucasfernog](https://github.com/tauri-apps/plugins-workspace/../../lucasfernog)) Update to tauri beta.24. + +## \[2.0.0-beta.7] + +- [`76daee7a`](https://github.com/tauri-apps/plugins-workspace/commit/76daee7aafece34de3092c86e531cf9eb1138989) ([#1512](https://github.com/tauri-apps/plugins-workspace/pull/1512) by [@renovate](https://github.com/tauri-apps/plugins-workspace/../../renovate)) Update to tauri beta.23. + +## \[2.0.0-beta.6] + +- [`9013854f`](https://github.com/tauri-apps/plugins-workspace/commit/9013854f42a49a230b9dbb9d02774765528a923f)([#1382](https://github.com/tauri-apps/plugins-workspace/pull/1382)) Update to tauri beta.22. + +## \[2.0.0-beta.5] + +- [`430bd6f4`](https://github.com/tauri-apps/plugins-workspace/commit/430bd6f4f379bee5d232ae6b098ae131db7f178a)([#1363](https://github.com/tauri-apps/plugins-workspace/pull/1363)) Update to tauri beta.20. + +## \[2.0.0-beta.4] + +- [`eb1679b9`](https://github.com/tauri-apps/plugins-workspace/commit/eb1679b99780e5d2b867f5649a1ccc2f3f70ab56)([#1299](https://github.com/tauri-apps/plugins-workspace/pull/1299)) Fix `Command.execute` API including extra new lines. +- [`eb1679b9`](https://github.com/tauri-apps/plugins-workspace/commit/eb1679b99780e5d2b867f5649a1ccc2f3f70ab56)([#1299](https://github.com/tauri-apps/plugins-workspace/pull/1299)) Improve the speed of the JS `Command.execute` API + +## \[2.0.0-beta.3] + +- [`bd1ed590`](https://github.com/tauri-apps/plugins-workspace/commit/bd1ed5903ffcce5500310dac1e59e8c67674ef1e)([#1237](https://github.com/tauri-apps/plugins-workspace/pull/1237)) Update to tauri beta.17. + +## \[2.0.0-beta.3] + +- [`a04ea2f`](https://github.com/tauri-apps/plugins-workspace/commit/a04ea2f38294d5a3987578283badc8eec87a7752)([#1071](https://github.com/tauri-apps/plugins-workspace/pull/1071)) The global API script is now only added to the binary when the `withGlobalTauri` config is true. +- [`040004a`](https://github.com/tauri-apps/plugins-workspace/commit/040004a6b9fbb89161d1b5764d79428dfe693776)([#1069](https://github.com/tauri-apps/plugins-workspace/pull/1069)) Change shell's schema property name `command` to `cmd`. + +## \[2.0.0-beta.2] + +- [`9586eab`](https://github.com/tauri-apps/plugins-workspace/commit/9586eabd5a96673e4d976757777f470ae358d68a)([#1021](https://github.com/tauri-apps/plugins-workspace/pull/1021)) On Windows, fix `open` can't open file if the file is being used by a program. +- [`99bea25`](https://github.com/tauri-apps/plugins-workspace/commit/99bea2559c2c0648c2519c50a18cd124dacef57b)([#1005](https://github.com/tauri-apps/plugins-workspace/pull/1005)) Update to tauri beta.8. + +## \[2.0.0-beta.1] + +- [`569defb`](https://github.com/tauri-apps/plugins-workspace/commit/569defbe9492e38938554bb7bdc1be9151456d21) Update to tauri beta.4. + +## \[2.0.0-beta.0] + +- [`d198c01`](https://github.com/tauri-apps/plugins-workspace/commit/d198c014863ee260cb0de88a14b7fc4356ef7474)([#862](https://github.com/tauri-apps/plugins-workspace/pull/862)) Update to tauri beta. + +## \[2.0.0-alpha.5] + +- [`387c2f9`](https://github.com/tauri-apps/plugins-workspace/commit/387c2f9e0ce4c75c07ffa3fd76391a25b58f5daf)([#802](https://github.com/tauri-apps/plugins-workspace/pull/802)) Update to @tauri-apps/api v2.0.0-alpha.13. + +## \[2.0.0-alpha.4] + +- [`387c2f9`](https://github.com/tauri-apps/plugins-workspace/commit/387c2f9e0ce4c75c07ffa3fd76391a25b58f5daf)([#802](https://github.com/tauri-apps/plugins-workspace/pull/802)) Update to @tauri-apps/api v2.0.0-alpha.12. + +## \[2.0.0-alpha.3] + +- [`e438e0a`](https://github.com/tauri-apps/plugins-workspace/commit/e438e0a62d4b430a5159f05f13ecd397dd891a0d)([#676](https://github.com/tauri-apps/plugins-workspace/pull/676)) Update to @tauri-apps/api v2.0.0-alpha.11. + +## \[2.0.0-alpha.2] + +- [`5c13736`](https://github.com/tauri-apps/plugins-workspace/commit/5c137365c60790e8d4037d449e8237aa3fffdab0)([#673](https://github.com/tauri-apps/plugins-workspace/pull/673)) Update to @tauri-apps/api v2.0.0-alpha.9. + +## \[2.0.0-alpha.2] + +- [`4e2cef9`](https://github.com/tauri-apps/plugins-workspace/commit/4e2cef9b702bbbb9cf4ee17de50791cb21f1b2a4)([#593](https://github.com/tauri-apps/plugins-workspace/pull/593)) Update to alpha.12. + +## \[2.0.0-alpha.1] + +- [`d74fc0a`](https://github.com/tauri-apps/plugins-workspace/commit/d74fc0a097996e90a37be8f57d50b7d1f6ca616f)([#555](https://github.com/tauri-apps/plugins-workspace/pull/555)) Update to alpha.11. + +## \[2.0.0-alpha.0] + +- [`717ae67`](https://github.com/tauri-apps/plugins-workspace/commit/717ae670978feb4492fac1f295998b93f2b9347f)([#371](https://github.com/tauri-apps/plugins-workspace/pull/371)) First v2 alpha release! diff --git a/plugins/shell/Cargo.toml b/plugins/shell/Cargo.toml new file mode 100644 index 0000000..105ea5f --- /dev/null +++ b/plugins/shell/Cargo.toml @@ -0,0 +1,49 @@ +[package] +name = "tauri-plugin-shell" +version = "2.3.6" +description = "Access the system shell. Allows you to spawn child processes and manage files and URLs using their default application." +edition = { workspace = true } +authors = { workspace = true } +license = { workspace = true } +rust-version = { workspace = true } +repository = { workspace = true } +links = "tauri-plugin-shell" +exclude = [ + "/banner.png", + "/guest-js", + "/package.json", + "/rollup.config.js", + "/test", + "/tsconfig.json", +] + +[package.metadata.platforms.support] +windows = { level = "full", notes = "" } +linux = { level = "full", notes = "" } +macos = { level = "full", notes = "" } +android = { level = "partial", notes = "Only allows to open URLs via `open`" } +ios = { level = "partial", notes = "Only allows to open URLs via `open`" } + +[build-dependencies] +tauri-plugin = { workspace = true, features = ["build"] } +schemars = { workspace = true } +serde = { workspace = true } + +[dev-dependencies] +tauri = { workspace = true, features = ["wry"] } + +[dependencies] +serde = { workspace = true } +serde_json = { workspace = true } +tauri = { workspace = true } +tokio = { version = "1", features = ["time"] } +log = { workspace = true } +thiserror = { workspace = true } +shared_child = "1" +regex = "1" +open = { version = "5", features = ["shellexecute-on-windows"] } +encoding_rs = "0.8" +os_pipe = "1" + +[target.'cfg(target_os = "ios")'.dependencies] +tauri = { workspace = true, features = ["wry"] } diff --git a/plugins/shell/LICENSE.spdx b/plugins/shell/LICENSE.spdx new file mode 100644 index 0000000..cdd0df5 --- /dev/null +++ b/plugins/shell/LICENSE.spdx @@ -0,0 +1,20 @@ +SPDXVersion: SPDX-2.1 +DataLicense: CC0-1.0 +PackageName: tauri +DataFormat: SPDXRef-1 +PackageSupplier: Organization: The Tauri Programme in the Commons Conservancy +PackageHomePage: https://tauri.app +PackageLicenseDeclared: Apache-2.0 +PackageLicenseDeclared: MIT +PackageCopyrightText: 2019-2022, The Tauri Programme in the Commons Conservancy +PackageSummary: Tauri is a rust project that enables developers to make secure +and small desktop applications using a web frontend. + +PackageComment: The package includes the following libraries; see +Relationship information. + +Created: 2019-05-20T09:00:00Z +PackageDownloadLocation: git://github.com/tauri-apps/tauri +PackageDownloadLocation: git+https://github.com/tauri-apps/tauri.git +PackageDownloadLocation: git+ssh://github.com/tauri-apps/tauri.git +Creator: Person: Daniel Thompson-Yvetot \ No newline at end of file diff --git a/plugins/shell/LICENSE_APACHE-2.0 b/plugins/shell/LICENSE_APACHE-2.0 new file mode 100644 index 0000000..4947287 --- /dev/null +++ b/plugins/shell/LICENSE_APACHE-2.0 @@ -0,0 +1,177 @@ + + 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 \ No newline at end of file diff --git a/plugins/shell/LICENSE_MIT b/plugins/shell/LICENSE_MIT new file mode 100644 index 0000000..4d75472 --- /dev/null +++ b/plugins/shell/LICENSE_MIT @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2017 - Present Tauri Apps 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. \ No newline at end of file diff --git a/plugins/shell/README.md b/plugins/shell/README.md new file mode 100644 index 0000000..132d1cc --- /dev/null +++ b/plugins/shell/README.md @@ -0,0 +1,90 @@ +![plugin-shell](https://github.com/tauri-apps/plugins-workspace/raw/v2/plugins/shell/banner.png) + +Access the system shell. Allows you to spawn child processes and manage files and URLs using their default application. + +| Platform | Supported | +| -------- | --------- | +| Linux | ✓ | +| Windows | ✓ | +| macOS | ✓ | +| Android | ✓ | +| iOS | ✓ | + +## Install + +_This plugin requires a Rust version of at least **1.77.2**_ + +There are three general methods of installation that we can recommend. + +1. Use crates.io and npm (easiest, and requires you to trust that our publishing pipeline worked) +2. Pull sources directly from Github using git tags / revision hashes (most secure) +3. Git submodule install this repo in your tauri project and then use file protocol to ingest the source (most secure, but inconvenient to use) + +Install the Core plugin by adding the following to your `Cargo.toml` file: + +`src-tauri/Cargo.toml` + +```toml +[dependencies] +tauri-plugin-shell = "2.0.0" +# alternatively with Git: +tauri-plugin-shell = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v2" } +``` + +You can install the JavaScript Guest bindings using your preferred JavaScript package manager: + +```sh +pnpm add @tauri-apps/plugin-shell +# or +npm add @tauri-apps/plugin-shell +# or +yarn add @tauri-apps/plugin-shell +``` + +## Usage + +First you need to register the core plugin with Tauri: + +`src-tauri/src/lib.rs` + +```rust +fn main() { + tauri::Builder::default() + .plugin(tauri_plugin_shell::init()) + .run(tauri::generate_context!()) + .expect("error while running tauri application"); +} +``` + +Afterwards all the plugin's APIs are available through the JavaScript guest bindings: + +```javascript +import { Command } from '@tauri-apps/plugin-shell' +Command.create('git', ['commit', '-m', 'the commit message']) +``` + +## Contributing + +PRs accepted. Please make sure to read the Contributing Guide before making a pull request. + +## Partners + + + + + + + +
+ + CrabNebula + +
+ +For the complete list of sponsors please visit our [website](https://tauri.app#sponsors) and [Open Collective](https://opencollective.com/tauri). + +## License + +Code: (c) 2015 - Present - The Tauri Programme within The Commons Conservancy. + +MIT or MIT/Apache 2.0 where applicable. diff --git a/plugins/shell/SECURITY.md b/plugins/shell/SECURITY.md new file mode 100644 index 0000000..4f09bba --- /dev/null +++ b/plugins/shell/SECURITY.md @@ -0,0 +1,23 @@ +# Security Policy + +**Do not report security vulnerabilities through public GitHub issues.** + +**Please use the [Private Vulnerability Disclosure](https://docs.github.com/en/code-security/security-advisories/guidance-on-reporting-and-writing-information-about-vulnerabilities/privately-reporting-a-security-vulnerability#privately-reporting-a-security-vulnerability) feature of GitHub.** + +Include as much of the following information: + +- Type of issue (e.g. improper input parsing, privilege escalation, etc.) +- The location of the affected source code (tag/branch/commit or direct URL) +- Any special configuration required to reproduce the issue +- The distribution affected or used to help us with reproduction of the issue +- Step-by-step instructions to reproduce the issue +- Ideally a reproduction repository +- Impact of the issue, including how an attacker might exploit the issue + +We prefer to receive reports in English. + +## Contact + +Please disclose a vulnerability or security relevant issue here: [https://github.com/tauri-apps/plugins-workspace/security/advisories/new](https://github.com/tauri-apps/plugins-workspace/security/advisories/new). + +Alternatively, you can also contact us by email via [security@tauri.app](mailto:security@tauri.app). diff --git a/plugins/shell/android/.gitignore b/plugins/shell/android/.gitignore new file mode 100644 index 0000000..c0f21ec --- /dev/null +++ b/plugins/shell/android/.gitignore @@ -0,0 +1,2 @@ +/build +/.tauri diff --git a/plugins/shell/android/build.gradle.kts b/plugins/shell/android/build.gradle.kts new file mode 100644 index 0000000..3f3ca0e --- /dev/null +++ b/plugins/shell/android/build.gradle.kts @@ -0,0 +1,30 @@ +plugins { + id("com.android.library") + id("org.jetbrains.kotlin.android") +} + +android { + namespace = "app.tauri.shell" + compileSdk = 36 + + defaultConfig { + minSdk = 24 + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + consumerProguardFiles("consumer-rules.pro") + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 + } + kotlinOptions { + jvmTarget = "1.8" + } +} + +dependencies { + implementation("androidx.core:core-ktx:1.9.0") + implementation("com.fasterxml.jackson.core:jackson-databind:2.15.3") + implementation(project(":tauri-android")) +} diff --git a/plugins/shell/android/consumer-rules.pro b/plugins/shell/android/consumer-rules.pro new file mode 100644 index 0000000..481bb43 --- /dev/null +++ b/plugins/shell/android/consumer-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/plugins/shell/android/settings.gradle b/plugins/shell/android/settings.gradle new file mode 100644 index 0000000..14a752e --- /dev/null +++ b/plugins/shell/android/settings.gradle @@ -0,0 +1,2 @@ +include ':tauri-android' +project(':tauri-android').projectDir = new File('./.tauri/tauri-api') diff --git a/plugins/shell/android/src/main/AndroidManifest.xml b/plugins/shell/android/src/main/AndroidManifest.xml new file mode 100644 index 0000000..9a40236 --- /dev/null +++ b/plugins/shell/android/src/main/AndroidManifest.xml @@ -0,0 +1,3 @@ + + + diff --git a/plugins/shell/android/src/main/java/ShellPlugin.kt b/plugins/shell/android/src/main/java/ShellPlugin.kt new file mode 100644 index 0000000..2268bc2 --- /dev/null +++ b/plugins/shell/android/src/main/java/ShellPlugin.kt @@ -0,0 +1,30 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +package app.tauri.shell + +import android.app.Activity +import android.content.Intent +import android.net.Uri +import app.tauri.annotation.Command +import app.tauri.annotation.TauriPlugin +import app.tauri.plugin.Invoke +import app.tauri.plugin.Plugin +import java.io.File + +@TauriPlugin +class ShellPlugin(private val activity: Activity) : Plugin(activity) { + @Command + fun open(invoke: Invoke) { + try { + val url = invoke.parseArgs(String::class.java) + val intent = Intent(Intent.ACTION_VIEW, Uri.parse(url)) + intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + activity.applicationContext?.startActivity(intent) + invoke.resolve() + } catch (ex: Exception) { + invoke.reject(ex.message) + } + } +} \ No newline at end of file diff --git a/plugins/shell/api-iife.js b/plugins/shell/api-iife.js new file mode 100644 index 0000000..ee35a32 --- /dev/null +++ b/plugins/shell/api-iife.js @@ -0,0 +1 @@ +if("__TAURI__"in window){var __TAURI_PLUGIN_SHELL__=function(e,t){"use strict";class s{constructor(){this.eventListeners=Object.create(null)}addListener(e,t){return this.on(e,t)}removeListener(e,t){return this.off(e,t)}on(e,t){return e in this.eventListeners?this.eventListeners[e].push(t):this.eventListeners[e]=[t],this}once(e,t){const s=n=>{this.removeListener(e,s),t(n)};return this.addListener(e,s)}off(e,t){return e in this.eventListeners&&(this.eventListeners[e]=this.eventListeners[e].filter(e=>e!==t)),this}removeAllListeners(e){return e?delete this.eventListeners[e]:this.eventListeners=Object.create(null),this}emit(e,t){if(e in this.eventListeners){const s=this.eventListeners[e];for(const e of s)e(t);return!0}return!1}listenerCount(e){return e in this.eventListeners?this.eventListeners[e].length:0}prependListener(e,t){return e in this.eventListeners?this.eventListeners[e].unshift(t):this.eventListeners[e]=[t],this}prependOnceListener(e,t){const s=n=>{this.removeListener(e,s),t(n)};return this.prependListener(e,s)}}class n{constructor(e){this.pid=e}async write(e){await t.invoke("plugin:shell|stdin_write",{pid:this.pid,buffer:e})}async kill(){await t.invoke("plugin:shell|kill",{cmd:"killChild",pid:this.pid})}}class i extends s{constructor(e,t=[],n){super(),this.stdout=new s,this.stderr=new s,this.program=e,this.args="string"==typeof t?[t]:t,this.options=n??{}}static create(e,t=[],s){return new i(e,t,s)}static sidecar(e,t=[],s){const n=new i(e,t,s);return n.options.sidecar=!0,n}async spawn(){const e=this.program,s=this.args,i=this.options;"object"==typeof s&&Object.freeze(s);const r=new t.Channel;return r.onmessage=e=>{switch(e.event){case"Error":this.emit("error",e.payload);break;case"Terminated":this.emit("close",e.payload);break;case"Stdout":this.stdout.emit("data",e.payload);break;case"Stderr":this.stderr.emit("data",e.payload)}},await t.invoke("plugin:shell|spawn",{program:e,args:s,options:i,onEvent:r}).then(e=>new n(e))}async execute(){const e=this.program,s=this.args,n=this.options;return"object"==typeof s&&Object.freeze(s),await t.invoke("plugin:shell|execute",{program:e,args:s,options:n})}}return e.Child=n,e.Command=i,e.EventEmitter=s,e.open=async function(e,s){await t.invoke("plugin:shell|open",{path:e,with:s})},e}({},window.__TAURI__.core);Object.defineProperty(window.__TAURI__,"shell",{value:__TAURI_PLUGIN_SHELL__})} diff --git a/plugins/shell/banner.png b/plugins/shell/banner.png new file mode 100644 index 0000000000000000000000000000000000000000..cbf748182b61d564f536e50fd3f6b9c5a2807900 GIT binary patch literal 52105 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 z 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 literal 0 HcmV?d00001 diff --git a/plugins/shell/build.rs b/plugins/shell/build.rs new file mode 100644 index 0000000..4e19ccd --- /dev/null +++ b/plugins/shell/build.rs @@ -0,0 +1,189 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use std::path::PathBuf; + +use schemars::JsonSchema; + +#[path = "src/scope_entry.rs"] +mod scope_entry; + +/// A command argument allowed to be executed by the webview API. +#[derive(Debug, PartialEq, Eq, Clone, Hash, schemars::JsonSchema)] +#[serde(untagged, deny_unknown_fields)] +#[non_exhaustive] +pub enum ShellScopeEntryAllowedArg { + /// A non-configurable argument that is passed to the command in the order it was specified. + Fixed(String), + + /// A variable that is set while calling the command from the webview API. + /// + Var { + /// [regex] validator to require passed values to conform to an expected input. + /// + /// This will require the argument value passed to this variable to match the `validator` regex + /// before it will be executed. + /// + /// The regex string is by default surrounded by `^...$` to match the full string. + /// For example the `https?://\w+` regex would be registered as `^https?://\w+$`. + /// + /// [regex]: + validator: String, + + /// Marks the validator as a raw regex, meaning the plugin should not make any modification at runtime. + /// + /// This means the regex will not match on the entire string by default, which might + /// be exploited if your regex allow unexpected input to be considered valid. + /// When using this option, make sure your regex is correct. + #[serde(default)] + raw: bool, + }, +} + +/// A set of command arguments allowed to be executed by the webview API. +/// +/// A value of `true` will allow any arguments to be passed to the command. `false` will disable all +/// arguments. A list of [`ShellScopeEntryAllowedArg`] will set those arguments as the only valid arguments to +/// be passed to the attached command configuration. +#[derive(Debug, PartialEq, Eq, Clone, Hash, JsonSchema)] +#[serde(untagged, deny_unknown_fields)] +#[non_exhaustive] +pub enum ShellScopeEntryAllowedArgs { + /// Use a simple boolean to allow all or disable all arguments to this command configuration. + Flag(bool), + + /// A specific set of [`ShellScopeEntryAllowedArg`] that are valid to call for the command configuration. + List(Vec), +} + +impl Default for ShellScopeEntryAllowedArgs { + fn default() -> Self { + Self::Flag(false) + } +} + +/// Shell scope entry. +#[derive(JsonSchema)] +#[serde(untagged, deny_unknown_fields)] +#[allow(unused)] +pub(crate) enum ShellScopeEntry { + Command { + /// The name for this allowed shell command configuration. + /// + /// This name will be used inside of the webview API to call this command along with + /// any specified arguments. + name: String, + /// The command name. + /// It can start with a variable that resolves to a system base directory. + /// The variables are: `$AUDIO`, `$CACHE`, `$CONFIG`, `$DATA`, `$LOCALDATA`, `$DESKTOP`, + /// `$DOCUMENT`, `$DOWNLOAD`, `$EXE`, `$FONT`, `$HOME`, `$PICTURE`, `$PUBLIC`, `$RUNTIME`, + /// `$TEMPLATE`, `$VIDEO`, `$RESOURCE`, `$LOG`, `$TEMP`, `$APPCONFIG`, `$APPDATA`, + /// `$APPLOCALDATA`, `$APPCACHE`, `$APPLOG`. + // use default just so the schema doesn't flag it as required + #[serde(rename = "cmd")] + command: PathBuf, + /// The allowed arguments for the command execution. + #[serde(default)] + args: ShellScopeEntryAllowedArgs, + }, + Sidecar { + /// The name for this allowed shell command configuration. + /// + /// This name will be used inside of the webview API to call this command along with + /// any specified arguments. + name: String, + /// The allowed arguments for the command execution. + #[serde(default)] + args: ShellScopeEntryAllowedArgs, + /// If this command is a sidecar command. + sidecar: bool, + }, +} + +// Ensure `ShellScopeEntry` and `scope_entry::EntryRaw` +// and `ShellScopeEntryAllowedArg` and `ShellAllowedArg` +// and `ShellScopeEntryAllowedArgs` and `ShellAllowedArgs` +// are kept in sync +#[allow(clippy::unnecessary_operation)] +fn _f() { + match (ShellScopeEntry::Sidecar { + name: String::new(), + args: ShellScopeEntryAllowedArgs::Flag(false), + sidecar: true, + }) { + ShellScopeEntry::Command { + name, + command, + args, + } => scope_entry::EntryRaw { + name, + command: Some(command), + args: match args { + ShellScopeEntryAllowedArgs::Flag(flag) => scope_entry::ShellAllowedArgs::Flag(flag), + ShellScopeEntryAllowedArgs::List(vec) => scope_entry::ShellAllowedArgs::List( + vec.into_iter() + .map(|s| match s { + ShellScopeEntryAllowedArg::Fixed(fixed) => { + scope_entry::ShellAllowedArg::Fixed(fixed) + } + ShellScopeEntryAllowedArg::Var { validator, raw } => { + scope_entry::ShellAllowedArg::Var { validator, raw } + } + }) + .collect(), + ), + }, + sidecar: false, + }, + ShellScopeEntry::Sidecar { + name, + args, + sidecar, + } => scope_entry::EntryRaw { + name, + command: None, + args: match args { + ShellScopeEntryAllowedArgs::Flag(flag) => scope_entry::ShellAllowedArgs::Flag(flag), + ShellScopeEntryAllowedArgs::List(vec) => scope_entry::ShellAllowedArgs::List( + vec.into_iter() + .map(|s| match s { + ShellScopeEntryAllowedArg::Fixed(fixed) => { + scope_entry::ShellAllowedArg::Fixed(fixed) + } + ShellScopeEntryAllowedArg::Var { validator, raw } => { + scope_entry::ShellAllowedArg::Var { validator, raw } + } + }) + .collect(), + ), + }, + sidecar, + }, + }; +} + +const COMMANDS: &[&str] = &["execute", "spawn", "stdin_write", "kill", "open"]; + +fn main() { + tauri_plugin::Builder::new(COMMANDS) + .global_api_script_path("./api-iife.js") + .global_scope_schema(schemars::schema_for!(ShellScopeEntry)) + .android_path("android") + .ios_path("ios") + .build(); + + let target_os = std::env::var("CARGO_CFG_TARGET_OS").unwrap(); + let mobile = target_os == "ios" || target_os == "android"; + alias("desktop", !mobile); + alias("mobile", mobile); +} + +// creates a cfg alias if `has_feature` is true. +// `alias` must be a snake case string. +fn alias(alias: &str, has_feature: bool) { + println!("cargo:rustc-check-cfg=cfg({alias})"); + if has_feature { + println!("cargo:rustc-cfg={alias}"); + } +} diff --git a/plugins/shell/guest-js/index.ts b/plugins/shell/guest-js/index.ts new file mode 100644 index 0000000..afac302 --- /dev/null +++ b/plugins/shell/guest-js/index.ts @@ -0,0 +1,928 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +/** + * Access the system shell. + * Allows you to spawn child processes and manage files and URLs using their default application. + * + * ## Security + * + * This API has a scope configuration that forces you to restrict the programs and arguments that can be used. + * + * ### Restricting access to the {@link open | `open`} API + * + * On the configuration object, `open: true` means that the {@link open} API can be used with any URL, + * as the argument is validated with the `^((mailto:\w+)|(tel:\w+)|(https?://\w+)).+` regex. + * You can change that regex by changing the boolean value to a string, e.g. `open: ^https://github.com/`. + * + * ### Restricting access to the {@link Command | `Command`} APIs + * + * The plugin permissions object has a `scope` field that defines an array of CLIs that can be used. + * Each CLI is a configuration object `{ name: string, cmd: string, sidecar?: bool, args?: boolean | Arg[] }`. + * + * - `name`: the unique identifier of the command, passed to the {@link Command.create | Command.create function}. + * If it's a sidecar, this must be the value defined on `tauri.conf.json > bundle > externalBin`. + * - `cmd`: the program that is executed on this configuration. If it's a sidecar, this value is ignored. + * - `sidecar`: whether the object configures a sidecar or a system program. + * - `args`: the arguments that can be passed to the program. By default no arguments are allowed. + * - `true` means that any argument list is allowed. + * - `false` means that no arguments are allowed. + * - otherwise an array can be configured. Each item is either a string representing the fixed argument value + * or a `{ validator: string }` that defines a regex validating the argument value. + * + * #### Example scope configuration + * + * CLI: `git commit -m "the commit message"` + * + * Capability: + * ```json + * { + * "permissions": [ + * { + * "identifier": "shell:allow-execute", + * "allow": [ + * { + * "name": "run-git-commit", + * "cmd": "git", + * "args": ["commit", "-m", { "validator": "\\S+" }] + * } + * ] + * } + * ] + * } + * ``` + * Usage: + * ```typescript + * import { Command } from '@tauri-apps/plugin-shell' + * Command.create('run-git-commit', ['commit', '-m', 'the commit message']) + * ``` + * + * Trying to execute any API with a program not configured on the scope results in a promise rejection due to denied access. + * + * @module + */ + +import { invoke, Channel } from '@tauri-apps/api/core' + +/** + * Options that configure how a child process is spawned. + * + * @since 2.0.0 + */ +interface SpawnOptions { + /** Current working directory. */ + cwd?: string + /** Environment variables. set to `null` to clear the process env. */ + env?: Record + /** + * Character encoding for stdout/stderr + * + * @since 2.0.0 + * */ + encoding?: string +} + +/** @ignore */ +interface InternalSpawnOptions extends SpawnOptions { + sidecar?: boolean +} + +/** + * The output collected from a child process that ran to completion. + * + * @since 2.0.0 + */ +interface ChildProcess { + /** Exit code of the process. `null` if the process was terminated by a signal on Unix. */ + code: number | null + /** If the process was terminated by a signal, represents that signal. */ + signal: number | null + /** The data that the process wrote to `stdout`. */ + stdout: O + /** The data that the process wrote to `stderr`. */ + stderr: O +} + +/** + * A minimal event emitter modeled after Node.js' `EventEmitter`, used by + * {@link Command} and by its `stdout` and `stderr` streams. + * + * @since 2.0.0 + */ +// eslint-disable-next-line @typescript-eslint/no-explicit-any +class EventEmitter> { + /** @ignore */ + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-explicit-any + private eventListeners: Record void>> = + Object.create(null) + + /** + * Alias for `emitter.on(eventName, listener)`. + * + * @example + * ```typescript + * import { Command } from '@tauri-apps/plugin-shell'; + * const command = Command.create('node'); + * command.addListener('error', (error) => console.error(error)); + * ``` + * + * @param eventName The name of the event to listen to. + * @param listener The callback invoked with the event payload. + * + * @returns A reference to the `EventEmitter`, so that calls can be chained. + * + * @since 2.0.0 + */ + addListener( + eventName: N, + listener: (arg: E[typeof eventName]) => void + ): this { + return this.on(eventName, listener) + } + + /** + * Alias for `emitter.off(eventName, listener)`. + * + * @example + * ```typescript + * import { Command } from '@tauri-apps/plugin-shell'; + * const command = Command.create('node'); + * const listener = (error: string) => console.error(error); + * command.addListener('error', listener); + * command.removeListener('error', listener); + * ``` + * + * @param eventName The name of the event to stop listening to. + * @param listener The exact callback that was registered before. + * + * @returns A reference to the `EventEmitter`, so that calls can be chained. + * + * @since 2.0.0 + */ + removeListener( + eventName: N, + listener: (arg: E[typeof eventName]) => void + ): this { + return this.off(eventName, listener) + } + + /** + * Adds the `listener` function to the end of the listeners array for the + * event named `eventName`. No checks are made to see if the `listener` has + * already been added. Multiple calls passing the same combination of `eventName`and `listener` will result in the `listener` being added, and called, multiple + * times. + * + * Returns a reference to the `EventEmitter`, so that calls can be chained. + * + * @example + * ```typescript + * import { Command } from '@tauri-apps/plugin-shell'; + * const command = Command.create('node'); + * command.on('close', (data) => { + * console.log(`command finished with code ${data.code}`); + * }); + * ``` + * + * @param eventName The name of the event to listen to. + * @param listener The callback invoked with the event payload. + * + * @returns A reference to the `EventEmitter`, so that calls can be chained. + * + * @since 2.0.0 + */ + on( + eventName: N, + listener: (arg: E[typeof eventName]) => void + ): this { + if (eventName in this.eventListeners) { + // eslint-disable-next-line security/detect-object-injection + this.eventListeners[eventName].push(listener) + } else { + // eslint-disable-next-line security/detect-object-injection + this.eventListeners[eventName] = [listener] + } + return this + } + + /** + * Adds a **one-time**`listener` function for the event named `eventName`. The + * next time `eventName` is triggered, this listener is removed and then invoked. + * + * Returns a reference to the `EventEmitter`, so that calls can be chained. + * + * @example + * ```typescript + * import { Command } from '@tauri-apps/plugin-shell'; + * const command = Command.create('node'); + * command.once('close', (data) => { + * console.log(`command finished with code ${data.code}`); + * }); + * ``` + * + * @param eventName The name of the event to listen to once. + * @param listener The callback invoked with the event payload. + * + * @returns A reference to the `EventEmitter`, so that calls can be chained. + * + * @since 2.0.0 + */ + once( + eventName: N, + listener: (arg: E[typeof eventName]) => void + ): this { + const wrapper = (arg: E[typeof eventName]): void => { + this.removeListener(eventName, wrapper) + listener(arg) + } + return this.addListener(eventName, wrapper) + } + + /** + * Removes the all specified listener from the listener array for the event eventName + * Returns a reference to the `EventEmitter`, so that calls can be chained. + * + * @example + * ```typescript + * import { Command } from '@tauri-apps/plugin-shell'; + * const command = Command.create('node'); + * const listener = (error: string) => console.error(error); + * command.on('error', listener); + * command.off('error', listener); + * ``` + * + * @param eventName The name of the event to stop listening to. + * @param listener The exact callback that was registered before. + * + * @returns A reference to the `EventEmitter`, so that calls can be chained. + * + * @since 2.0.0 + */ + off( + eventName: N, + listener: (arg: E[typeof eventName]) => void + ): this { + if (eventName in this.eventListeners) { + // eslint-disable-next-line security/detect-object-injection + this.eventListeners[eventName] = this.eventListeners[eventName].filter( + (l) => l !== listener + ) + } + return this + } + + /** + * Removes all listeners, or those of the specified eventName. + * + * Returns a reference to the `EventEmitter`, so that calls can be chained. + * + * @example + * ```typescript + * import { Command } from '@tauri-apps/plugin-shell'; + * const command = Command.create('node'); + * command.on('error', (error) => console.error(error)); + * command.removeAllListeners('error'); + * ``` + * + * @param event The name of the event to remove the listeners of. + * When omitted, the listeners of every event are removed. + * + * @returns A reference to the `EventEmitter`, so that calls can be chained. + * + * @since 2.0.0 + */ + removeAllListeners(event?: N): this { + if (event) { + // eslint-disable-next-line security/detect-object-injection + delete this.eventListeners[event] + } else { + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + this.eventListeners = Object.create(null) + } + return this + } + + /** + * Synchronously calls each of the listeners registered for the event named + * `eventName`, in the order they were registered, passing the supplied arguments + * to each. + * + * @ignore + * + * @example + * ```typescript + * import { EventEmitter } from '@tauri-apps/plugin-shell'; + * const emitter = new EventEmitter<{ data: string }>(); + * emitter.on('data', (line) => console.log(line)); + * emitter.emit('data', 'hello'); + * ``` + * + * @param eventName The name of the event to emit. + * @param arg The payload passed to every registered listener. + * + * @returns `true` if the event had listeners, `false` otherwise. + * + * @since 2.0.0 + */ + emit(eventName: N, arg: E[typeof eventName]): boolean { + if (eventName in this.eventListeners) { + // eslint-disable-next-line security/detect-object-injection + const listeners = this.eventListeners[eventName] + for (const listener of listeners) listener(arg) + return true + } + return false + } + + /** + * Returns the number of listeners listening to the event named `eventName`. + * + * @example + * ```typescript + * import { Command } from '@tauri-apps/plugin-shell'; + * const command = Command.create('node'); + * command.on('close', () => {}); + * console.log(command.listenerCount('close')); // 1 + * ``` + * + * @param eventName The name of the event to count the listeners of. + * + * @returns The number of listeners registered for the given event. + * + * @since 2.0.0 + */ + listenerCount(eventName: N): number { + if (eventName in this.eventListeners) + // eslint-disable-next-line security/detect-object-injection + return this.eventListeners[eventName].length + return 0 + } + + /** + * Adds the `listener` function to the _beginning_ of the listeners array for the + * event named `eventName`. No checks are made to see if the `listener` has + * already been added. Multiple calls passing the same combination of `eventName`and `listener` will result in the `listener` being added, and called, multiple + * times. + * + * Returns a reference to the `EventEmitter`, so that calls can be chained. + * + * @example + * ```typescript + * import { Command } from '@tauri-apps/plugin-shell'; + * const command = Command.create('node'); + * command.prependListener('error', (error) => console.error(error)); + * ``` + * + * @param eventName The name of the event to listen to. + * @param listener The callback invoked with the event payload. + * + * @returns A reference to the `EventEmitter`, so that calls can be chained. + * + * @since 2.0.0 + */ + prependListener( + eventName: N, + listener: (arg: E[typeof eventName]) => void + ): this { + if (eventName in this.eventListeners) { + // eslint-disable-next-line security/detect-object-injection + this.eventListeners[eventName].unshift(listener) + } else { + // eslint-disable-next-line security/detect-object-injection + this.eventListeners[eventName] = [listener] + } + return this + } + + /** + * Adds a **one-time**`listener` function for the event named `eventName` to the_beginning_ of the listeners array. The next time `eventName` is triggered, this + * listener is removed, and then invoked. + * + * Returns a reference to the `EventEmitter`, so that calls can be chained. + * + * @example + * ```typescript + * import { Command } from '@tauri-apps/plugin-shell'; + * const command = Command.create('node'); + * command.prependOnceListener('error', (error) => console.error(error)); + * ``` + * + * @param eventName The name of the event to listen to once. + * @param listener The callback invoked with the event payload. + * + * @returns A reference to the `EventEmitter`, so that calls can be chained. + * + * @since 2.0.0 + */ + prependOnceListener( + eventName: N, + listener: (arg: E[typeof eventName]) => void + ): this { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const wrapper = (arg: any): void => { + this.removeListener(eventName, wrapper) + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument + listener(arg) + } + return this.prependListener(eventName, wrapper) + } +} + +/** + * A handle to a child process spawned with {@link Command.spawn}, + * which can be used to write to its `stdin` or to kill it. + * + * @since 2.0.0 + */ +class Child { + /** The child process `pid`. */ + pid: number + + /** + * Creates a handle to the child process with the given process id. + * + * @example + * ```typescript + * import { Command } from '@tauri-apps/plugin-shell'; + * // a `Child` is usually obtained by spawning a command: + * const child = await Command.create('node').spawn(); + * console.log(child.pid); + * ``` + * + * @param pid The process id of the child process. + * + * @since 2.0.0 + */ + constructor(pid: number) { + this.pid = pid + } + + /** + * Writes `data` to the `stdin`. + * + * @param data The message to write, either a string or a byte array. + * @example + * ```typescript + * import { Command } from '@tauri-apps/plugin-shell'; + * const command = Command.create('node'); + * const child = await command.spawn(); + * await child.write('message'); + * await child.write([0, 1, 2, 3, 4, 5]); + * ``` + * + * @returns A promise indicating the success or failure of the operation. + * + * @since 2.0.0 + */ + async write(data: IOPayload | number[]): Promise { + await invoke('plugin:shell|stdin_write', { + pid: this.pid, + buffer: data + }) + } + + /** + * Kills the child process. + * + * @example + * ```typescript + * import { Command } from '@tauri-apps/plugin-shell'; + * const command = Command.create('node'); + * const child = await command.spawn(); + * await child.kill(); + * ``` + * + * @returns A promise indicating the success or failure of the operation. + * + * @since 2.0.0 + */ + async kill(): Promise { + await invoke('plugin:shell|kill', { + cmd: 'killChild', + pid: this.pid + }) + } +} + +/** + * The events emitted by a {@link Command} instance. + * + * @since 2.0.0 + */ +interface CommandEvents { + /** Emitted when the child process terminated, carrying its exit code and signal. */ + close: TerminatedPayload + /** Emitted when the child process could not be spawned or failed unexpectedly, carrying the error message. */ + error: string +} + +/** + * The events emitted by the `stdout` and `stderr` streams of a {@link Command}. + * + * @since 2.0.0 + */ +interface OutputEvents { + /** Emitted for each line the process wrote to the stream, or for each raw chunk when the `raw` encoding is used. */ + data: O +} + +/** + * The entry point for spawning child processes. + * It emits the `close` and `error` events. + * @example + * ```typescript + * import { Command } from '@tauri-apps/plugin-shell'; + * const command = Command.create('node'); + * command.on('close', data => { + * console.log(`command finished with code ${data.code} and signal ${data.signal}`) + * }); + * command.on('error', error => console.error(`command error: "${error}"`)); + * command.stdout.on('data', line => console.log(`command stdout: "${line}"`)); + * command.stderr.on('data', line => console.log(`command stderr: "${line}"`)); + * + * const child = await command.spawn(); + * console.log('pid:', child.pid); + * ``` + * + * @since 2.0.0 + * + */ +class Command extends EventEmitter { + /** @ignore Program to execute. */ + private readonly program: string + /** @ignore Program arguments */ + private readonly args: string[] + /** @ignore Spawn options. */ + private readonly options: InternalSpawnOptions + /** Event emitter for the `stdout`. Emits the `data` event. */ + readonly stdout = new EventEmitter>() + /** Event emitter for the `stderr`. Emits the `data` event. */ + readonly stderr = new EventEmitter>() + + /** + * @ignore + * Creates a new `Command` instance. + * + * @param program The program name to execute. + * It must be configured in your project's capabilities. + * @param args Program arguments. + * @param options Spawn options. + */ + private constructor( + program: string, + args: string | string[] = [], + options?: SpawnOptions + ) { + super() + this.program = program + this.args = typeof args === 'string' ? [args] : args + this.options = options ?? {} + } + + /** + * Creates a command to execute the given program, decoding its output as text. + * + * @example + * ```typescript + * import { Command } from '@tauri-apps/plugin-shell'; + * const command = Command.create('my-app', ['run', 'tauri']); + * const output = await command.execute(); + * ``` + * + * @param program The program to execute. + * It must be configured in your project's capabilities. + * @param args The arguments to pass to the program. Defaults to no arguments. + * + * @returns The command instance, ready to be spawned or executed. + * + * @since 2.0.0 + */ + static create(program: string, args?: string | string[]): Command + /** + * Creates a command to execute the given program, keeping its output as raw bytes. + * + * @example + * ```typescript + * import { Command } from '@tauri-apps/plugin-shell'; + * const command = Command.create('my-app', ['run', 'tauri'], { encoding: 'raw' }); + * const output = await command.execute(); + * console.log(output.stdout); // a Uint8Array + * ``` + * + * @param program The program to execute. + * It must be configured in your project's capabilities. + * @param args The arguments to pass to the program. Defaults to no arguments. + * @param options Spawn options using the `raw` encoding, which makes the process + * output be delivered as `Uint8Array` instead of `string`. + * + * @returns The command instance, ready to be spawned or executed. + * + * @since 2.0.0 + */ + static create( + program: string, + args?: string | string[], + options?: SpawnOptions & { encoding: 'raw' } + ): Command + /** + * Creates a command to execute the given program with the given spawn options. + * + * @example + * ```typescript + * import { Command } from '@tauri-apps/plugin-shell'; + * const command = Command.create('my-app', ['run', 'tauri'], { cwd: '/path/to/project' }); + * const output = await command.execute(); + * ``` + * + * @param program The program to execute. + * It must be configured in your project's capabilities. + * @param args The arguments to pass to the program. Defaults to no arguments. + * @param options Spawn options such as the working directory, the environment + * variables and the character encoding of the process output. + * + * @returns The command instance, ready to be spawned or executed. + * + * @since 2.0.0 + */ + static create( + program: string, + args?: string | string[], + options?: SpawnOptions + ): Command + + /** + * Creates a command to execute the given program. + * + * @param program The program to execute. + * It must be configured in your project's capabilities. + * @param args The arguments to pass to the program. Defaults to no arguments. + * @param options Spawn options such as the working directory, the environment + * variables and the character encoding of the process output. + * + * @returns The command instance, ready to be spawned or executed. + * + * @since 2.0.0 + */ + static create( + program: string, + args: string | string[] = [], + options?: SpawnOptions + ): Command { + return new Command(program, args, options) + } + + /** + * Creates a command to execute the given sidecar program, decoding its output as text. + * + * @example + * ```typescript + * import { Command } from '@tauri-apps/plugin-shell'; + * const command = Command.sidecar('my-sidecar'); + * const output = await command.execute(); + * ``` + * + * @param program The sidecar program to execute. + * It must be configured in your project's capabilities + * and defined on `tauri.conf.json > bundle > externalBin`. + * @param args The arguments to pass to the program. Defaults to no arguments. + * + * @returns The command instance, ready to be spawned or executed. + * + * @since 2.0.0 + */ + static sidecar(program: string, args?: string | string[]): Command + /** + * Creates a command to execute the given sidecar program, keeping its output as raw bytes. + * + * @example + * ```typescript + * import { Command } from '@tauri-apps/plugin-shell'; + * const command = Command.sidecar('my-sidecar', [], { encoding: 'raw' }); + * const output = await command.execute(); + * console.log(output.stdout); // a Uint8Array + * ``` + * + * @param program The sidecar program to execute. + * It must be configured in your project's capabilities + * and defined on `tauri.conf.json > bundle > externalBin`. + * @param args The arguments to pass to the program. Defaults to no arguments. + * @param options Spawn options using the `raw` encoding, which makes the process + * output be delivered as `Uint8Array` instead of `string`. + * + * @returns The command instance, ready to be spawned or executed. + * + * @since 2.0.0 + */ + static sidecar( + program: string, + args?: string | string[], + options?: SpawnOptions & { encoding: 'raw' } + ): Command + /** + * Creates a command to execute the given sidecar program with the given spawn options. + * + * @example + * ```typescript + * import { Command } from '@tauri-apps/plugin-shell'; + * const command = Command.sidecar('my-sidecar', [], { cwd: '/path/to/project' }); + * const output = await command.execute(); + * ``` + * + * @param program The sidecar program to execute. + * It must be configured in your project's capabilities + * and defined on `tauri.conf.json > bundle > externalBin`. + * @param args The arguments to pass to the program. Defaults to no arguments. + * @param options Spawn options such as the working directory, the environment + * variables and the character encoding of the process output. + * + * @returns The command instance, ready to be spawned or executed. + * + * @since 2.0.0 + */ + static sidecar( + program: string, + args?: string | string[], + options?: SpawnOptions + ): Command + + /** + * Creates a command to execute the given sidecar program. + * + * @param program The sidecar program to execute. + * It must be configured in your project's capabilities + * and defined on `tauri.conf.json > bundle > externalBin`. + * @param args The arguments to pass to the program. Defaults to no arguments. + * @param options Spawn options such as the working directory, the environment + * variables and the character encoding of the process output. + * + * @returns The command instance, ready to be spawned or executed. + * + * @since 2.0.0 + */ + static sidecar( + program: string, + args: string | string[] = [], + options?: SpawnOptions + ): Command { + const instance = new Command(program, args, options) + instance.options.sidecar = true + return instance + } + + /** + * Executes the command as a child process, returning a handle to it. + * + * @example + * ```typescript + * import { Command } from '@tauri-apps/plugin-shell'; + * const command = Command.create('node'); + * command.stdout.on('data', (line) => console.log(line)); + * const child = await command.spawn(); + * console.log('pid:', child.pid); + * ``` + * + * @returns A promise resolving to the child process handle. + * + * @since 2.0.0 + */ + async spawn(): Promise { + const program = this.program + const args = this.args + const options = this.options + + if (typeof args === 'object') { + Object.freeze(args) + } + + const onEvent = new Channel>() + onEvent.onmessage = (event) => { + switch (event.event) { + case 'Error': + this.emit('error', event.payload) + break + case 'Terminated': + this.emit('close', event.payload) + break + case 'Stdout': + this.stdout.emit('data', event.payload) + break + case 'Stderr': + this.stderr.emit('data', event.payload) + break + } + } + + return await invoke('plugin:shell|spawn', { + program, + args, + options, + onEvent + }).then((pid) => new Child(pid)) + } + + /** + * Executes the command as a child process, waiting for it to finish and collecting all of its output. + * @example + * ```typescript + * import { Command } from '@tauri-apps/plugin-shell'; + * const output = await Command.create('echo', 'message').execute(); + * assert(output.code === 0); + * assert(output.signal === null); + * assert(output.stdout === 'message'); + * assert(output.stderr === ''); + * ``` + * + * @returns A promise resolving to the child process output. + * + * @since 2.0.0 + */ + async execute(): Promise> { + const program = this.program + const args = this.args + const options = this.options + + if (typeof args === 'object') { + Object.freeze(args) + } + + return await invoke>('plugin:shell|execute', { + program, + args, + options + }) + } +} + +/** + * Describes the event message received from the command. + */ +interface Event { + event: T + payload: V +} + +/** + * Payload for the `Terminated` command event. + */ +interface TerminatedPayload { + /** Exit code of the process. `null` if the process was terminated by a signal on Unix. */ + code: number | null + /** If the process was terminated by a signal, represents that signal. */ + signal: number | null +} + +/** + * The type of the data a child process writes to `stdout` and `stderr`: + * a `string`, or a `Uint8Array` when the `raw` encoding is configured. + */ +type IOPayload = string | Uint8Array + +/** Events emitted by the child process. */ +type CommandEvent = + | Event<'Stdout', O> + | Event<'Stderr', O> + | Event<'Terminated', TerminatedPayload> + | Event<'Error', string> + +/** + * Opens a path or URL with the system's default app, + * or the one specified with `openWith`. + * + * The `openWith` value must be one of `firefox`, `google chrome`, `chromium` `safari`, + * `open`, `start`, `xdg-open`, `gio`, `gnome-open`, `kde-open` or `wslview`. + * + * @example + * ```typescript + * import { open } from '@tauri-apps/plugin-shell'; + * // opens the given URL on the default browser: + * await open('https://github.com/tauri-apps/tauri'); + * // opens the given URL using `firefox`: + * await open('https://github.com/tauri-apps/tauri', 'firefox'); + * // opens a file using the default program: + * await open('/path/to/file'); + * ``` + * + * @param path The path or URL to open. + * This value is matched against the string regex defined on `tauri.conf.json > plugins > shell > open`, + * which defaults to `^((mailto:\w+)|(tel:\w+)|(https?://\w+)).+`. + * @param openWith The app to open the file or URL with. + * Defaults to the system default application for the specified path type. + * + * @since 2.0.0 + */ +async function open(path: string, openWith?: string): Promise { + await invoke('plugin:shell|open', { + path, + with: openWith + }) +} + +export { Command, Child, EventEmitter, open } +export type { + IOPayload, + CommandEvents, + TerminatedPayload, + OutputEvents, + ChildProcess, + SpawnOptions +} diff --git a/plugins/shell/guest-js/init.ts b/plugins/shell/guest-js/init.ts new file mode 100644 index 0000000..d0ed650 --- /dev/null +++ b/plugins/shell/guest-js/init.ts @@ -0,0 +1,51 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +/** + * Webview script injected by the shell plugin on every page load. + * + * It installs a `click` listener on `document.body` that intercepts clicks on + * `
` elements whose `href` starts with `http://`, `https://`, + * `mailto:` or `tel:`, cancels the navigation and opens the link with the + * system's default application through the `plugin:shell|open` command instead. + * + * @module + */ + +import { invoke } from '@tauri-apps/api/core' + +// open links with the API +function openLinks(): void { + document.querySelector('body')?.addEventListener('click', function (e) { + let target: HTMLElement | null = e.target as HTMLElement + while (target) { + if (target.matches('a')) { + const t = target + if ( + t.href !== '' + && ['http://', 'https://', 'mailto:', 'tel:'].some((v) => + t.href.startsWith(v) + ) + && t.target === '_blank' + ) { + void invoke('plugin:shell|open', { + path: t.href + }) + e.preventDefault() + } + break + } + target = target.parentElement + } + }) +} + +if ( + document.readyState === 'complete' + || document.readyState === 'interactive' +) { + openLinks() +} else { + window.addEventListener('DOMContentLoaded', openLinks, true) +} diff --git a/plugins/shell/ios/Package.resolved b/plugins/shell/ios/Package.resolved new file mode 100644 index 0000000..5f998e0 --- /dev/null +++ b/plugins/shell/ios/Package.resolved @@ -0,0 +1,16 @@ +{ + "object": { + "pins": [ + { + "package": "SwiftRs", + "repositoryURL": "https://github.com/Brendonovich/swift-rs", + "state": { + "branch": null, + "revision": "b5ed223fcdab165bc21219c1925dc1e77e2bef5e", + "version": "1.0.6" + } + } + ] + }, + "version": 1 +} diff --git a/plugins/shell/ios/Package.swift b/plugins/shell/ios/Package.swift new file mode 100644 index 0000000..c7b2a7a --- /dev/null +++ b/plugins/shell/ios/Package.swift @@ -0,0 +1,34 @@ +// swift-tools-version:5.3 +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +import PackageDescription + +let package = Package( + name: "tauri-plugin-shell", + platforms: [ + .macOS(.v10_13), + .iOS(.v13), + ], + products: [ + // Products define the executables and libraries a package produces, and make them visible to other packages. + .library( + name: "tauri-plugin-shell", + type: .static, + targets: ["tauri-plugin-shell"]) + ], + dependencies: [ + .package(name: "Tauri", path: "../.tauri/tauri-api") + ], + targets: [ + // Targets are the basic building blocks of a package. A target can define a module or a test suite. + // Targets can depend on other targets in this package, and on products in packages this package depends on. + .target( + name: "tauri-plugin-shell", + dependencies: [ + .byName(name: "Tauri") + ], + path: "Sources") + ] +) diff --git a/plugins/shell/ios/Sources/ShellPlugin.swift b/plugins/shell/ios/Sources/ShellPlugin.swift new file mode 100644 index 0000000..0fcb7da --- /dev/null +++ b/plugins/shell/ios/Sources/ShellPlugin.swift @@ -0,0 +1,34 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +import Foundation + +import SwiftRs +import Tauri +import UIKit +import WebKit + +class ShellPlugin: Plugin { + + @objc public func open(_ invoke: Invoke) throws { + do { + let urlString = try invoke.parseArgs(String.self) + if let url = URL(string: urlString) { + if #available(iOS 10, *) { + UIApplication.shared.open(url, options: [:]) + } else { + UIApplication.shared.openURL(url) + } + } + invoke.resolve() + } catch { + invoke.reject(error.localizedDescription) + } + } +} + +@_cdecl("init_plugin_shell") +func initPlugin() -> Plugin { + return ShellPlugin() +} diff --git a/plugins/shell/package.json b/plugins/shell/package.json new file mode 100644 index 0000000..cf5ef5c --- /dev/null +++ b/plugins/shell/package.json @@ -0,0 +1,29 @@ +{ + "name": "@tauri-apps/plugin-shell", + "version": "2.3.6", + "license": "MIT OR Apache-2.0", + "authors": [ + "Tauri Programme within The Commons Conservancy" + ], + "repository": "https://github.com/tauri-apps/plugins-workspace", + "type": "module", + "types": "./dist-js/index.d.ts", + "main": "./dist-js/index.cjs", + "module": "./dist-js/index.js", + "exports": { + "types": "./dist-js/index.d.ts", + "import": "./dist-js/index.js", + "require": "./dist-js/index.cjs" + }, + "scripts": { + "build": "rollup -c" + }, + "files": [ + "dist-js", + "README.md", + "LICENSE" + ], + "dependencies": { + "@tauri-apps/api": "^2.11.0" + } +} diff --git a/plugins/shell/permissions/autogenerated/commands/execute.toml b/plugins/shell/permissions/autogenerated/commands/execute.toml new file mode 100644 index 0000000..d98be89 --- /dev/null +++ b/plugins/shell/permissions/autogenerated/commands/execute.toml @@ -0,0 +1,13 @@ +# Automatically generated - DO NOT EDIT! + +"$schema" = "../../schemas/schema.json" + +[[permission]] +identifier = "allow-execute" +description = "Enables the execute command without any pre-configured scope." +commands.allow = ["execute"] + +[[permission]] +identifier = "deny-execute" +description = "Denies the execute command without any pre-configured scope." +commands.deny = ["execute"] diff --git a/plugins/shell/permissions/autogenerated/commands/kill.toml b/plugins/shell/permissions/autogenerated/commands/kill.toml new file mode 100644 index 0000000..11d2f7f --- /dev/null +++ b/plugins/shell/permissions/autogenerated/commands/kill.toml @@ -0,0 +1,13 @@ +# Automatically generated - DO NOT EDIT! + +"$schema" = "../../schemas/schema.json" + +[[permission]] +identifier = "allow-kill" +description = "Enables the kill command without any pre-configured scope." +commands.allow = ["kill"] + +[[permission]] +identifier = "deny-kill" +description = "Denies the kill command without any pre-configured scope." +commands.deny = ["kill"] diff --git a/plugins/shell/permissions/autogenerated/commands/open.toml b/plugins/shell/permissions/autogenerated/commands/open.toml new file mode 100644 index 0000000..4ea6dff --- /dev/null +++ b/plugins/shell/permissions/autogenerated/commands/open.toml @@ -0,0 +1,13 @@ +# Automatically generated - DO NOT EDIT! + +"$schema" = "../../schemas/schema.json" + +[[permission]] +identifier = "allow-open" +description = "Enables the open command without any pre-configured scope." +commands.allow = ["open"] + +[[permission]] +identifier = "deny-open" +description = "Denies the open command without any pre-configured scope." +commands.deny = ["open"] diff --git a/plugins/shell/permissions/autogenerated/commands/spawn.toml b/plugins/shell/permissions/autogenerated/commands/spawn.toml new file mode 100644 index 0000000..a3802d2 --- /dev/null +++ b/plugins/shell/permissions/autogenerated/commands/spawn.toml @@ -0,0 +1,13 @@ +# Automatically generated - DO NOT EDIT! + +"$schema" = "../../schemas/schema.json" + +[[permission]] +identifier = "allow-spawn" +description = "Enables the spawn command without any pre-configured scope." +commands.allow = ["spawn"] + +[[permission]] +identifier = "deny-spawn" +description = "Denies the spawn command without any pre-configured scope." +commands.deny = ["spawn"] diff --git a/plugins/shell/permissions/autogenerated/commands/stdin_write.toml b/plugins/shell/permissions/autogenerated/commands/stdin_write.toml new file mode 100644 index 0000000..46ca5a7 --- /dev/null +++ b/plugins/shell/permissions/autogenerated/commands/stdin_write.toml @@ -0,0 +1,13 @@ +# Automatically generated - DO NOT EDIT! + +"$schema" = "../../schemas/schema.json" + +[[permission]] +identifier = "allow-stdin-write" +description = "Enables the stdin_write command without any pre-configured scope." +commands.allow = ["stdin_write"] + +[[permission]] +identifier = "deny-stdin-write" +description = "Denies the stdin_write command without any pre-configured scope." +commands.deny = ["stdin_write"] diff --git a/plugins/shell/permissions/autogenerated/reference.md b/plugins/shell/permissions/autogenerated/reference.md new file mode 100644 index 0000000..87086a1 --- /dev/null +++ b/plugins/shell/permissions/autogenerated/reference.md @@ -0,0 +1,154 @@ +## Default Permission + +This permission set configures which +shell functionality is exposed by default. + +#### Granted Permissions + +It allows to use the `open` functionality with a reasonable +scope pre-configured. It will allow opening `http(s)://`, +`tel:` and `mailto:` links. + +#### This default permission set includes the following: + +- `allow-open` + +## Permission Table + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
IdentifierDescription
+ +`shell:allow-execute` + + + +Enables the execute command without any pre-configured scope. + +
+ +`shell:deny-execute` + + + +Denies the execute command without any pre-configured scope. + +
+ +`shell:allow-kill` + + + +Enables the kill command without any pre-configured scope. + +
+ +`shell:deny-kill` + + + +Denies the kill command without any pre-configured scope. + +
+ +`shell:allow-open` + + + +Enables the open command without any pre-configured scope. + +
+ +`shell:deny-open` + + + +Denies the open command without any pre-configured scope. + +
+ +`shell:allow-spawn` + + + +Enables the spawn command without any pre-configured scope. + +
+ +`shell:deny-spawn` + + + +Denies the spawn command without any pre-configured scope. + +
+ +`shell:allow-stdin-write` + + + +Enables the stdin_write command without any pre-configured scope. + +
+ +`shell:deny-stdin-write` + + + +Denies the stdin_write command without any pre-configured scope. + +
diff --git a/plugins/shell/permissions/default.toml b/plugins/shell/permissions/default.toml new file mode 100644 index 0000000..dba2ea2 --- /dev/null +++ b/plugins/shell/permissions/default.toml @@ -0,0 +1,15 @@ +"$schema" = "schemas/schema.json" + +[default] +description = """ +This permission set configures which +shell functionality is exposed by default. + +#### Granted Permissions + +It allows to use the `open` functionality with a reasonable +scope pre-configured. It will allow opening `http(s)://`, +`tel:` and `mailto:` links. +""" + +permissions = ["allow-open"] diff --git a/plugins/shell/permissions/schemas/schema.json b/plugins/shell/permissions/schemas/schema.json new file mode 100644 index 0000000..9a19898 --- /dev/null +++ b/plugins/shell/permissions/schemas/schema.json @@ -0,0 +1,366 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "PermissionFile", + "description": "Permission file that can define a default permission, a set of permissions or a list of inlined permissions.", + "type": "object", + "properties": { + "default": { + "description": "The default permission set for the plugin", + "anyOf": [ + { + "$ref": "#/definitions/DefaultPermission" + }, + { + "type": "null" + } + ] + }, + "set": { + "description": "A list of permissions sets defined", + "type": "array", + "items": { + "$ref": "#/definitions/PermissionSet" + } + }, + "permission": { + "description": "A list of inlined permissions", + "default": [], + "type": "array", + "items": { + "$ref": "#/definitions/Permission" + } + } + }, + "definitions": { + "DefaultPermission": { + "description": "The default permission set of the plugin.\n\nWorks similarly to a permission with the \"default\" identifier.", + "type": "object", + "required": [ + "permissions" + ], + "properties": { + "version": { + "description": "The version of the permission.", + "type": [ + "integer", + "null" + ], + "format": "uint64", + "minimum": 1.0 + }, + "description": { + "description": "Human-readable description of what the permission does. Tauri convention is to use `

` headings in markdown content for Tauri documentation generation purposes.", + "type": [ + "string", + "null" + ] + }, + "permissions": { + "description": "All permissions this set contains.", + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "PermissionSet": { + "description": "A set of direct permissions grouped together under a new name.", + "type": "object", + "required": [ + "description", + "identifier", + "permissions" + ], + "properties": { + "identifier": { + "description": "A unique identifier for the permission.", + "type": "string" + }, + "description": { + "description": "Human-readable description of what the permission does.", + "type": "string" + }, + "permissions": { + "description": "All permissions this set contains.", + "type": "array", + "items": { + "$ref": "#/definitions/PermissionKind" + } + } + } + }, + "Permission": { + "description": "Descriptions of explicit privileges of commands.\n\nIt can enable commands to be accessible in the frontend of the application.\n\nIf the scope is defined it can be used to fine grain control the access of individual or multiple commands.", + "type": "object", + "required": [ + "identifier" + ], + "properties": { + "version": { + "description": "The version of the permission.", + "type": [ + "integer", + "null" + ], + "format": "uint64", + "minimum": 1.0 + }, + "identifier": { + "description": "A unique identifier for the permission.", + "type": "string" + }, + "description": { + "description": "Human-readable description of what the permission does. Tauri internal convention is to use `

` headings in markdown content for Tauri documentation generation purposes.", + "type": [ + "string", + "null" + ] + }, + "commands": { + "description": "Allowed or denied commands when using this permission.", + "default": { + "allow": [], + "deny": [] + }, + "allOf": [ + { + "$ref": "#/definitions/Commands" + } + ] + }, + "scope": { + "description": "Allowed or denied scoped when using this permission.", + "allOf": [ + { + "$ref": "#/definitions/Scopes" + } + ] + }, + "platforms": { + "description": "Target platforms this permission applies. By default all platforms are affected by this permission.", + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/Target" + } + } + } + }, + "Commands": { + "description": "Allowed and denied commands inside a permission.\n\nIf two commands clash inside of `allow` and `deny`, it should be denied by default.", + "type": "object", + "properties": { + "allow": { + "description": "Allowed command.", + "default": [], + "type": "array", + "items": { + "type": "string" + } + }, + "deny": { + "description": "Denied command, which takes priority.", + "default": [], + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "Scopes": { + "description": "An argument for fine grained behavior control of Tauri commands.\n\nIt can be of any serde serializable type and is used to allow or prevent certain actions inside a Tauri command. The configured scope is passed to the command and will be enforced by the command implementation.\n\n## Example\n\n```json { \"allow\": [{ \"path\": \"$HOME/**\" }], \"deny\": [{ \"path\": \"$HOME/secret.txt\" }] } ```", + "type": "object", + "properties": { + "allow": { + "description": "Data that defines what is allowed by the scope.", + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/Value" + } + }, + "deny": { + "description": "Data that defines what is denied by the scope. This should be prioritized by validation logic.", + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/Value" + } + } + } + }, + "Value": { + "description": "All supported ACL values.", + "anyOf": [ + { + "description": "Represents a null JSON value.", + "type": "null" + }, + { + "description": "Represents a [`bool`].", + "type": "boolean" + }, + { + "description": "Represents a valid ACL [`Number`].", + "allOf": [ + { + "$ref": "#/definitions/Number" + } + ] + }, + { + "description": "Represents a [`String`].", + "type": "string" + }, + { + "description": "Represents a list of other [`Value`]s.", + "type": "array", + "items": { + "$ref": "#/definitions/Value" + } + }, + { + "description": "Represents a map of [`String`] keys to [`Value`]s.", + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/Value" + } + } + ] + }, + "Number": { + "description": "A valid ACL number.", + "anyOf": [ + { + "description": "Represents an [`i64`].", + "type": "integer", + "format": "int64" + }, + { + "description": "Represents a [`f64`].", + "type": "number", + "format": "double" + } + ] + }, + "Target": { + "description": "Platform target.", + "oneOf": [ + { + "description": "MacOS.", + "type": "string", + "enum": [ + "macOS" + ] + }, + { + "description": "Windows.", + "type": "string", + "enum": [ + "windows" + ] + }, + { + "description": "Linux.", + "type": "string", + "enum": [ + "linux" + ] + }, + { + "description": "Android.", + "type": "string", + "enum": [ + "android" + ] + }, + { + "description": "iOS.", + "type": "string", + "enum": [ + "iOS" + ] + } + ] + }, + "PermissionKind": { + "type": "string", + "oneOf": [ + { + "description": "Enables the execute command without any pre-configured scope.", + "type": "string", + "const": "allow-execute", + "markdownDescription": "Enables the execute command without any pre-configured scope." + }, + { + "description": "Denies the execute command without any pre-configured scope.", + "type": "string", + "const": "deny-execute", + "markdownDescription": "Denies the execute command without any pre-configured scope." + }, + { + "description": "Enables the kill command without any pre-configured scope.", + "type": "string", + "const": "allow-kill", + "markdownDescription": "Enables the kill command without any pre-configured scope." + }, + { + "description": "Denies the kill command without any pre-configured scope.", + "type": "string", + "const": "deny-kill", + "markdownDescription": "Denies the kill command without any pre-configured scope." + }, + { + "description": "Enables the open command without any pre-configured scope.", + "type": "string", + "const": "allow-open", + "markdownDescription": "Enables the open command without any pre-configured scope." + }, + { + "description": "Denies the open command without any pre-configured scope.", + "type": "string", + "const": "deny-open", + "markdownDescription": "Denies the open command without any pre-configured scope." + }, + { + "description": "Enables the spawn command without any pre-configured scope.", + "type": "string", + "const": "allow-spawn", + "markdownDescription": "Enables the spawn command without any pre-configured scope." + }, + { + "description": "Denies the spawn command without any pre-configured scope.", + "type": "string", + "const": "deny-spawn", + "markdownDescription": "Denies the spawn command without any pre-configured scope." + }, + { + "description": "Enables the stdin_write command without any pre-configured scope.", + "type": "string", + "const": "allow-stdin-write", + "markdownDescription": "Enables the stdin_write command without any pre-configured scope." + }, + { + "description": "Denies the stdin_write command without any pre-configured scope.", + "type": "string", + "const": "deny-stdin-write", + "markdownDescription": "Denies the stdin_write command without any pre-configured scope." + }, + { + "description": "This permission set configures which\nshell functionality is exposed by default.\n\n#### Granted Permissions\n\nIt allows to use the `open` functionality with a reasonable\nscope pre-configured. It will allow opening `http(s)://`,\n`tel:` and `mailto:` links.\n\n#### This default permission set includes:\n\n- `allow-open`", + "type": "string", + "const": "default", + "markdownDescription": "This permission set configures which\nshell functionality is exposed by default.\n\n#### Granted Permissions\n\nIt allows to use the `open` functionality with a reasonable\nscope pre-configured. It will allow opening `http(s)://`,\n`tel:` and `mailto:` links.\n\n#### This default permission set includes:\n\n- `allow-open`" + } + ] + } + } +} \ No newline at end of file diff --git a/plugins/shell/rollup.config.js b/plugins/shell/rollup.config.js new file mode 100644 index 0000000..a7dbd4f --- /dev/null +++ b/plugins/shell/rollup.config.js @@ -0,0 +1,22 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +import { createConfig } from '../../shared/rollup.config.js' +import { nodeResolve } from '@rollup/plugin-node-resolve' +import typescript from '@rollup/plugin-typescript' +import terser from '@rollup/plugin-terser' + +export default createConfig({ + additionalConfigs: { + input: 'guest-js/init.ts', + output: { + file: 'src/init-iife.js', + format: 'iife' + }, + plugins: [typescript(), terser(), nodeResolve()], + onwarn: (warning) => { + throw Object.assign(new Error(), warning) + } + } +}) diff --git a/plugins/shell/src/commands.rs b/plugins/shell/src/commands.rs new file mode 100644 index 0000000..0facce7 --- /dev/null +++ b/plugins/shell/src/commands.rs @@ -0,0 +1,320 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use std::{collections::HashMap, future::Future, path::PathBuf, pin::Pin, string::FromUtf8Error}; + +use encoding_rs::Encoding; +use serde::{Deserialize, Serialize}; +use tauri::{ + ipc::{Channel, CommandScope, GlobalScope}, + Manager, Runtime, State, Window, +}; + +#[allow(deprecated)] +use crate::open::Program; +use crate::{ + process::{CommandEvent, TerminatedPayload}, + scope::ExecuteArgs, + Shell, +}; + +type ChildId = u32; + +#[derive(Debug, Clone, Serialize)] +#[serde(tag = "event", content = "payload")] +#[non_exhaustive] +pub enum JSCommandEvent { + /// Stderr bytes until a newline (\n) or carriage return (\r) is found. + Stderr(Buffer), + /// Stdout bytes until a newline (\n) or carriage return (\r) is found. + Stdout(Buffer), + /// An error happened waiting for the command to finish or converting the stdout/stderr bytes to an UTF-8 string. + Error(String), + /// Command process terminated. + Terminated(TerminatedPayload), +} + +fn get_event_buffer(line: Vec, encoding: EncodingWrapper) -> Result { + match encoding { + EncodingWrapper::Text(character_encoding) => match character_encoding { + Some(encoding) => Ok(Buffer::Text( + encoding.decode_with_bom_removal(&line).0.into(), + )), + None => String::from_utf8(line).map(Buffer::Text), + }, + EncodingWrapper::Raw => Ok(Buffer::Raw(line)), + } +} + +impl JSCommandEvent { + pub fn new(event: CommandEvent, encoding: EncodingWrapper) -> Self { + match event { + CommandEvent::Terminated(payload) => JSCommandEvent::Terminated(payload), + CommandEvent::Error(error) => JSCommandEvent::Error(error), + CommandEvent::Stderr(line) => get_event_buffer(line, encoding) + .map(JSCommandEvent::Stderr) + .unwrap_or_else(|e| JSCommandEvent::Error(e.to_string())), + CommandEvent::Stdout(line) => get_event_buffer(line, encoding) + .map(JSCommandEvent::Stdout) + .unwrap_or_else(|e| JSCommandEvent::Error(e.to_string())), + } + } +} + +#[derive(Debug, Clone, Deserialize, Serialize)] +#[serde(untagged)] +#[allow(missing_docs)] +pub enum Buffer { + Text(String), + Raw(Vec), +} + +#[derive(Debug, Copy, Clone)] +pub enum EncodingWrapper { + Raw, + Text(Option<&'static Encoding>), +} + +#[derive(Debug, Clone, Default, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct CommandOptions { + #[serde(default)] + sidecar: bool, + cwd: Option, + // by default we don't add any env variables to the spawned process + // but the env is an `Option` so when it's `None` we clear the env. + #[serde(default = "default_env")] + env: Option>, + // Character encoding for stdout/stderr + encoding: Option, +} + +#[allow(clippy::unnecessary_wraps)] +fn default_env() -> Option> { + Some(HashMap::default()) +} + +#[inline(always)] +fn prepare_cmd( + window: Window, + program: String, + args: ExecuteArgs, + options: CommandOptions, + command_scope: CommandScope, + global_scope: GlobalScope, +) -> crate::Result<(crate::process::Command, EncodingWrapper)> { + let scope = crate::scope::ShellScope { + scopes: command_scope + .allows() + .iter() + .chain(global_scope.allows()) + .collect(), + }; + + let mut command = if options.sidecar { + let program = PathBuf::from(program); + let program_as_string = program.display().to_string(); + let has_extension = program.extension().is_some_and(|ext| ext == "exe"); + let program_no_ext_as_string = if has_extension { + program.with_extension("").display().to_string() + } else { + program_as_string.clone() + }; + let configured_sidecar = window + .config() + .bundle + .external_bin + .as_ref() + .and_then(|bins| { + bins.iter() + .find(|b| b == &&program_as_string || b == &&program_no_ext_as_string) + }) + .cloned(); + if let Some(sidecar) = configured_sidecar { + scope.prepare_sidecar(&program.to_string_lossy(), &sidecar, args)? + } else { + return Err(crate::Error::SidecarNotAllowed(program)); + } + } else { + match scope.prepare(&program, args) { + Ok(cmd) => cmd, + Err(e) => { + #[cfg(debug_assertions)] + eprintln!("{e}"); + return Err(crate::Error::ProgramNotAllowed(PathBuf::from(program))); + } + } + }; + if let Some(cwd) = options.cwd { + command = command.current_dir(cwd); + } + if let Some(env) = options.env { + command = command.envs(env); + } else { + command = command.env_clear(); + } + + let encoding = match options.encoding { + Option::None => EncodingWrapper::Text(None), + Some(encoding) => match encoding.as_str() { + "raw" => { + command = command.set_raw_out(true); + EncodingWrapper::Raw + } + _ => { + if let Some(text_encoding) = Encoding::for_label(encoding.as_bytes()) { + EncodingWrapper::Text(Some(text_encoding)) + } else { + return Err(crate::Error::UnknownEncoding(encoding)); + } + } + }, + }; + + Ok((command, encoding)) +} + +#[derive(Serialize)] +#[serde(untagged)] +enum Output { + String(String), + Raw(Vec), +} + +#[derive(Serialize)] +pub struct ChildProcessReturn { + code: Option, + signal: Option, + stdout: Output, + stderr: Output, +} + +#[allow(clippy::too_many_arguments)] +#[tauri::command] +pub async fn execute( + window: Window, + program: String, + args: ExecuteArgs, + options: CommandOptions, + command_scope: CommandScope, + global_scope: GlobalScope, +) -> crate::Result { + let (command, encoding) = + prepare_cmd(window, program, args, options, command_scope, global_scope)?; + + let mut command: std::process::Command = command.into(); + let output = command.output()?; + + let (stdout, stderr) = match encoding { + EncodingWrapper::Text(Some(encoding)) => ( + Output::String(encoding.decode_with_bom_removal(&output.stdout).0.into()), + Output::String(encoding.decode_with_bom_removal(&output.stderr).0.into()), + ), + EncodingWrapper::Text(None) => ( + Output::String(String::from_utf8(output.stdout)?), + Output::String(String::from_utf8(output.stderr)?), + ), + EncodingWrapper::Raw => (Output::Raw(output.stdout), Output::Raw(output.stderr)), + }; + + #[cfg(unix)] + use std::os::unix::process::ExitStatusExt; + + Ok(ChildProcessReturn { + code: output.status.code(), + #[cfg(windows)] + signal: None, + #[cfg(unix)] + signal: output.status.signal(), + stdout, + stderr, + }) +} + +#[allow(clippy::too_many_arguments)] +#[tauri::command] +pub fn spawn( + window: Window, + shell: State<'_, Shell>, + program: String, + args: ExecuteArgs, + on_event: Channel, + options: CommandOptions, + command_scope: CommandScope, + global_scope: GlobalScope, +) -> crate::Result { + let (command, encoding) = + prepare_cmd(window, program, args, options, command_scope, global_scope)?; + + let (mut rx, child) = command.spawn()?; + + let pid = child.pid(); + shell.children.lock().unwrap().insert(pid, child); + let children = shell.children.clone(); + + tauri::async_runtime::spawn(async move { + while let Some(event) = rx.recv().await { + if matches!(event, crate::process::CommandEvent::Terminated(_)) { + children.lock().unwrap().remove(&pid); + }; + let js_event = JSCommandEvent::new(event, encoding); + + if on_event.send(js_event.clone()).is_err() { + fn send<'a>( + on_event: &'a Channel, + js_event: &'a JSCommandEvent, + ) -> Pin + Send + 'a>> { + Box::pin(async move { + tokio::time::sleep(std::time::Duration::from_millis(15)).await; + if on_event.send(js_event.clone()).is_err() { + send(on_event, js_event).await; + } + }) + } + send(&on_event, &js_event).await; + } + } + }); + + Ok(pid) +} + +#[tauri::command] +pub fn stdin_write( + _window: Window, + shell: State<'_, Shell>, + pid: ChildId, + buffer: Buffer, +) -> crate::Result<()> { + if let Some(child) = shell.children.lock().unwrap().get_mut(&pid) { + match buffer { + Buffer::Text(t) => child.write(t.as_bytes())?, + Buffer::Raw(r) => child.write(&r)?, + } + } + Ok(()) +} + +#[tauri::command] +pub fn kill( + _window: Window, + shell: State<'_, Shell>, + pid: ChildId, +) -> crate::Result<()> { + if let Some(child) = shell.children.lock().unwrap().remove(&pid) { + child.kill()?; + } + Ok(()) +} + +#[allow(deprecated)] +#[tauri::command] +pub async fn open( + _window: Window, + shell: State<'_, Shell>, + path: String, + with: Option, +) -> crate::Result<()> { + crate::open::open(Some(&shell.open_scope), path, with) +} diff --git a/plugins/shell/src/config.rs b/plugins/shell/src/config.rs new file mode 100644 index 0000000..1523c08 --- /dev/null +++ b/plugins/shell/src/config.rs @@ -0,0 +1,39 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use serde::Deserialize; + +/// Configuration for the shell plugin. +#[derive(Debug, Default, PartialEq, Eq, Clone, Deserialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct Config { + /// Open URL with the user's default application. + #[serde(default)] + pub open: ShellAllowlistOpen, +} + +/// Defines the `shell > open` api scope. +#[derive(Debug, PartialEq, Eq, Clone, Deserialize)] +#[serde(untagged, deny_unknown_fields)] +#[non_exhaustive] +#[derive(Default)] +pub enum ShellAllowlistOpen { + /// Shell open API allowlist is not defined by the user. + /// In this case we add the default validation regex (same as [`Self::Flag(true)`]). + #[default] + Unset, + /// If the shell open API should be enabled. + /// + /// If enabled, the default validation regex (`^((mailto:\w+)|(tel:\w+)|(https?://\w+)).+`) is used. + Flag(bool), + + /// Enable the shell open API, with a custom regex that the opened path must match against. + /// + /// The regex string is automatically surrounded by `^...$` to match the full string. + /// For example the `https?://\w+` regex would be registered as `^https?://\w+$`. + /// + /// If using a custom regex to support a non-http(s) schema, care should be used to prevent values + /// that allow flag-like strings to pass validation. e.g. `--enable-debugging`, `-i`, `/R`. + Validate(String), +} diff --git a/plugins/shell/src/error.rs b/plugins/shell/src/error.rs new file mode 100644 index 0000000..1e2edcf --- /dev/null +++ b/plugins/shell/src/error.rs @@ -0,0 +1,56 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use std::path::PathBuf; + +use serde::{Serialize, Serializer}; + +/// Errors returned by the shell plugin. +#[derive(Debug, thiserror::Error)] +pub enum Error { + /// Failed to invoke the mobile plugin implementation. + /// + /// Only available on mobile. + #[cfg(mobile)] + #[error(transparent)] + PluginInvoke(#[from] tauri::plugin::mobile::PluginInvokeError), + /// I/O error, usually raised when spawning a process or reading its output. + #[error(transparent)] + Io(#[from] std::io::Error), + /// The path of the current executable has no parent directory, + /// so the path of a sidecar program could not be resolved. + #[error("current executable path has no parent")] + CurrentExeHasNoParent, + /// The string does not match any of the known programs + /// accepted by the deprecated `open` API. + #[error("unknown program {0}")] + UnknownProgramName(String), + /// The command is not allowed by the configured shell scope. + #[error(transparent)] + Scope(#[from] crate::scope::Error), + /// Sidecar not allowed by the configuration. + #[error("sidecar not configured under `tauri.conf.json > bundle > externalBin`: {0}")] + SidecarNotAllowed(PathBuf), + /// Program not allowed by the scope. + #[error("program not allowed on the configured shell scope: {0}")] + ProgramNotAllowed(PathBuf), + /// The `encoding` spawn option is neither `raw` nor a label of a known character encoding. + #[error("unknown encoding {0}")] + UnknownEncoding(String), + /// JSON error. + #[error(transparent)] + Json(#[from] serde_json::Error), + /// Utf8 error. + #[error(transparent)] + Utf8(#[from] std::string::FromUtf8Error), +} + +impl Serialize for Error { + fn serialize(&self, serializer: S) -> std::result::Result + where + S: Serializer, + { + serializer.serialize_str(self.to_string().as_ref()) + } +} diff --git a/plugins/shell/src/init-iife.js b/plugins/shell/src/init-iife.js new file mode 100644 index 0000000..c584490 --- /dev/null +++ b/plugins/shell/src/init-iife.js @@ -0,0 +1 @@ +!function(){"use strict";async function e(e,t={},n){return window.__TAURI_INTERNALS__.invoke(e,t,n)}function t(){document.querySelector("body")?.addEventListener("click",function(t){let n=t.target;for(;n;){if(n.matches("a")){const r=n;""!==r.href&&["http://","https://","mailto:","tel:"].some(e=>r.href.startsWith(e))&&"_blank"===r.target&&(e("plugin:shell|open",{path:r.href}),t.preventDefault());break}n=n.parentElement}})}"function"==typeof SuppressedError&&SuppressedError,"complete"===document.readyState||"interactive"===document.readyState?t():window.addEventListener("DOMContentLoaded",t,!0)}(); diff --git a/plugins/shell/src/lib.rs b/plugins/shell/src/lib.rs new file mode 100644 index 0000000..2615b12 --- /dev/null +++ b/plugins/shell/src/lib.rs @@ -0,0 +1,195 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +//! Access the system shell. Allows you to spawn child processes and manage files and URLs using their default application. + +#![doc( + html_logo_url = "https://github.com/tauri-apps/tauri/raw/dev/app-icon.png", + html_favicon_url = "https://github.com/tauri-apps/tauri/raw/dev/app-icon.png" +)] + +use std::{ + collections::HashMap, + ffi::OsStr, + path::Path, + sync::{Arc, Mutex}, +}; + +use process::{Command, CommandChild}; +use regex::Regex; +use tauri::{ + plugin::{Builder, TauriPlugin}, + AppHandle, Manager, RunEvent, Runtime, +}; + +mod commands; +mod config; +mod error; +#[deprecated(since = "2.1.0", note = "Use tauri-plugin-opener instead.")] +#[allow(deprecated)] +pub mod open; +/// Types and helpers to spawn and interact with child processes. +pub mod process; +mod scope; +mod scope_entry; + +pub use error::Error; +type Result = std::result::Result; + +#[cfg(mobile)] +use tauri::plugin::PluginHandle; +#[cfg(target_os = "android")] +const PLUGIN_IDENTIFIER: &str = "app.tauri.shell"; +#[cfg(target_os = "ios")] +tauri::ios_plugin_binding!(init_plugin_shell); + +type ChildStore = Arc>>; + +/// Access to the shell APIs. +/// +/// Get an instance of this type with [`ShellExt::shell`]. +pub struct Shell { + #[allow(dead_code)] + app: AppHandle, + #[cfg(mobile)] + mobile_plugin_handle: PluginHandle, + open_scope: scope::OpenScope, + children: ChildStore, +} + +impl Shell { + /// Creates a new Command for launching the given program. + pub fn command(&self, program: impl AsRef) -> Command { + Command::new(program) + } + + /// Creates a new Command for launching the given sidecar program. + /// + /// A sidecar program is a embedded external binary in order to make your application work + /// or to prevent users having to install additional dependencies (e.g. Node.js, Python, etc). + pub fn sidecar(&self, program: impl AsRef) -> Result { + Command::new_sidecar(program) + } + + /// Open a (url) path with a default or specific browser opening program. + /// + /// See [`crate::open::open`] for how it handles security-related measures. + #[cfg(desktop)] + #[deprecated(since = "2.1.0", note = "Use tauri-plugin-opener instead.")] + #[allow(deprecated)] + pub fn open(&self, path: impl Into, with: Option) -> Result<()> { + open::open(None, path.into(), with) + } + + /// Open a (url) path with a default or specific browser opening program. + /// + /// See [`crate::open::open`] for how it handles security-related measures. + #[cfg(mobile)] + #[deprecated(since = "2.1.0", note = "Use tauri-plugin-opener instead.")] + pub fn open(&self, path: impl Into, _with: Option) -> Result<()> { + self.mobile_plugin_handle + .run_mobile_plugin("open", path.into()) + .map_err(Into::into) + } +} + +/// Extensions to [`tauri::App`], [`tauri::AppHandle`], [`tauri::WebviewWindow`], +/// [`tauri::Webview`] and [`tauri::Window`] to access the shell APIs. +pub trait ShellExt { + /// Gets the shell APIs. + /// + /// # Examples + /// + /// ```no_run + /// use tauri_plugin_shell::ShellExt; + /// + /// async fn run_echo(app: &tauri::AppHandle) { + /// let output = app.shell().command("echo").args(["hello"]).output().await.unwrap(); + /// println!("{}", String::from_utf8_lossy(&output.stdout)); + /// } + /// ``` + fn shell(&self) -> &Shell; +} + +impl> ShellExt for T { + fn shell(&self) -> &Shell { + self.state::>().inner() + } +} + +/// Initializes the shell plugin. +/// +/// The plugin state can be accessed with [`ShellExt::shell`], +/// and all spawned child processes are killed when the application exits. +/// +/// # Examples +/// +/// ```no_run +/// fn setup(builder: tauri::Builder) -> tauri::Builder { +/// builder.plugin(tauri_plugin_shell::init()) +/// } +/// ``` +pub fn init() -> TauriPlugin> { + Builder::>::new("shell") + .js_init_script(include_str!("init-iife.js").to_string()) + .invoke_handler(tauri::generate_handler![ + commands::execute, + commands::spawn, + commands::stdin_write, + commands::kill, + commands::open + ]) + .setup(|app, api| { + let default_config = config::Config::default(); + let config = api.config().as_ref().unwrap_or(&default_config); + + #[cfg(target_os = "android")] + let handle = api.register_android_plugin(PLUGIN_IDENTIFIER, "ShellPlugin")?; + #[cfg(target_os = "ios")] + let handle = api.register_ios_plugin(init_plugin_shell)?; + + app.manage(Shell { + app: app.clone(), + children: Default::default(), + open_scope: open_scope(&config.open), + + #[cfg(mobile)] + mobile_plugin_handle: handle, + }); + Ok(()) + }) + .on_event(|app, event| { + if let RunEvent::Exit = event { + let shell = app.state::>(); + let children = { + let mut lock = shell.children.lock().unwrap(); + std::mem::take(&mut *lock) + }; + for child in children.into_values() { + let _ = child.kill(); + } + } + }) + .build() +} + +fn open_scope(open: &config::ShellAllowlistOpen) -> scope::OpenScope { + let shell_scope_open = match open { + config::ShellAllowlistOpen::Flag(false) => None, + // we want to add a basic regex validation even if the config is not set + config::ShellAllowlistOpen::Unset | config::ShellAllowlistOpen::Flag(true) => { + Some(Regex::new(r"^((mailto:\w+)|(tel:\w+)|(https?://\w+)).+").unwrap()) + } + config::ShellAllowlistOpen::Validate(validator) => { + let regex = format!("^{validator}$"); + let validator = + Regex::new(®ex).unwrap_or_else(|e| panic!("invalid regex {regex}: {e}")); + Some(validator) + } + }; + + scope::OpenScope { + open: shell_scope_open, + } +} diff --git a/plugins/shell/src/open.rs b/plugins/shell/src/open.rs new file mode 100644 index 0000000..6958a83 --- /dev/null +++ b/plugins/shell/src/open.rs @@ -0,0 +1,138 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +//! Types and functions related to shell. + +use serde::{Deserialize, Deserializer}; + +use crate::scope::OpenScope; +use std::str::FromStr; + +/// Program to use on the [`open()`] call. +#[deprecated(since = "2.1.0", note = "Use tauri-plugin-opener instead.")] +pub enum Program { + /// Use the `open` program. + Open, + /// Use the `start` program. + Start, + /// Use the `xdg-open` program. + XdgOpen, + /// Use the `gio` program. + Gio, + /// Use the `gnome-open` program. + GnomeOpen, + /// Use the `kde-open` program. + KdeOpen, + /// Use the `wslview` program. + WslView, + /// Use the `Firefox` program. + Firefox, + /// Use the `Google Chrome` program. + Chrome, + /// Use the `Chromium` program. + Chromium, + /// Use the `Safari` program. + Safari, +} + +impl FromStr for Program { + type Err = super::Error; + + fn from_str(s: &str) -> Result { + let p = match s.to_lowercase().as_str() { + "open" => Self::Open, + "start" => Self::Start, + "xdg-open" => Self::XdgOpen, + "gio" => Self::Gio, + "gnome-open" => Self::GnomeOpen, + "kde-open" => Self::KdeOpen, + "wslview" => Self::WslView, + "firefox" => Self::Firefox, + "chrome" | "google chrome" => Self::Chrome, + "chromium" => Self::Chromium, + "safari" => Self::Safari, + _ => return Err(crate::Error::UnknownProgramName(s.to_string())), + }; + Ok(p) + } +} + +impl<'de> Deserialize<'de> for Program { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let s = String::deserialize(deserializer)?; + Program::from_str(&s).map_err(|e| serde::de::Error::custom(e.to_string())) + } +} + +impl Program { + pub(crate) fn name(self) -> &'static str { + match self { + Self::Open => "open", + Self::Start => "start", + Self::XdgOpen => "xdg-open", + Self::Gio => "gio", + Self::GnomeOpen => "gnome-open", + Self::KdeOpen => "kde-open", + Self::WslView => "wslview", + + #[cfg(target_os = "macos")] + Self::Firefox => "Firefox", + #[cfg(not(target_os = "macos"))] + Self::Firefox => "firefox", + + #[cfg(target_os = "macos")] + Self::Chrome => "Google Chrome", + #[cfg(not(target_os = "macos"))] + Self::Chrome => "google-chrome", + + #[cfg(target_os = "macos")] + Self::Chromium => "Chromium", + #[cfg(not(target_os = "macos"))] + Self::Chromium => "chromium", + + #[cfg(target_os = "macos")] + Self::Safari => "Safari", + #[cfg(not(target_os = "macos"))] + Self::Safari => "safari", + } + } +} + +/// Opens path or URL with the program specified in `with`, or system default if `None`. +/// +/// The path will be matched against the shell open validation regex, defaulting to `^((mailto:\w+)|(tel:\w+)|(https?://\w+)).+`. +/// A custom validation regex may be supplied in the config in `plugins > shell > scope > open`. +/// +/// # Examples +/// +/// ```rust,no_run +/// use tauri_plugin_shell::ShellExt; +/// tauri::Builder::default() +/// .setup(|app| { +/// // open the given URL on the system default browser +/// app.shell().open("https://github.com/tauri-apps/tauri", None)?; +/// Ok(()) +/// }); +/// ``` +#[deprecated(since = "2.1.0", note = "Use tauri-plugin-opener instead.")] +pub fn open>( + scope: Option<&OpenScope>, + path: P, + with: Option, +) -> crate::Result<()> { + // validate scope if we have any (JS calls) + if let Some(scope) = scope { + scope.open(path.as_ref(), with).map_err(Into::into) + } else { + // when running directly from Rust code we don't need to validate the path + match with.map(Program::name) { + Some(program) => ::open::with_detached(path.as_ref(), program), + None => ::open::that_detached(path.as_ref()), + } + .map_err(Into::into) + } +} diff --git a/plugins/shell/src/process/mod.rs b/plugins/shell/src/process/mod.rs new file mode 100644 index 0000000..7a1c2db --- /dev/null +++ b/plugins/shell/src/process/mod.rs @@ -0,0 +1,658 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use std::{ + ffi::OsStr, + io::{BufRead, BufReader, Write}, + path::{Path, PathBuf}, + process::{Command as StdCommand, Stdio}, + sync::{Arc, RwLock}, + thread::spawn, +}; + +#[cfg(unix)] +use std::os::unix::process::ExitStatusExt; +#[cfg(windows)] +use std::os::windows::process::CommandExt; + +#[cfg(windows)] +const CREATE_NO_WINDOW: u32 = 0x0800_0000; +const NEWLINE_BYTE: u8 = b'\n'; + +use tauri::async_runtime::{block_on as block_on_task, channel, Receiver, Sender}; + +pub use encoding_rs::Encoding; +use os_pipe::{pipe, PipeReader, PipeWriter}; +use serde::Serialize; +use shared_child::SharedChild; +use tauri::utils::platform; + +/// Payload for the [`CommandEvent::Terminated`] command event. +#[derive(Debug, Clone, Serialize)] +pub struct TerminatedPayload { + /// Exit code of the process. + pub code: Option, + /// If the process was terminated by a signal, represents that signal. + pub signal: Option, +} + +/// A event sent to the command callback. +#[derive(Debug, Clone)] +#[non_exhaustive] +pub enum CommandEvent { + /// If configured for raw output, all bytes written to stderr. + /// Otherwise, bytes until a newline (\n) or carriage return (\r) is found. + Stderr(Vec), + /// If configured for raw output, all bytes written to stdout. + /// Otherwise, bytes until a newline (\n) or carriage return (\r) is found. + Stdout(Vec), + /// An error happened waiting for the command to finish or converting the stdout/stderr bytes to a UTF-8 string. + Error(String), + /// Command process terminated. + Terminated(TerminatedPayload), +} + +/// The type to spawn commands. +#[derive(Debug)] +pub struct Command { + cmd: StdCommand, + raw_out: bool, +} + +/// Spawned child process. +#[derive(Debug)] +pub struct CommandChild { + inner: Arc, + stdin_writer: PipeWriter, +} + +impl CommandChild { + /// Writes to process stdin. + pub fn write(&mut self, buf: &[u8]) -> crate::Result<()> { + self.stdin_writer.write_all(buf)?; + Ok(()) + } + + /// Sends a kill signal to the child. + pub fn kill(self) -> crate::Result<()> { + self.inner.kill()?; + Ok(()) + } + + /// Returns the process pid. + pub fn pid(&self) -> u32 { + self.inner.id() + } +} + +/// Describes the result of a process after it has terminated. +#[derive(Debug)] +pub struct ExitStatus { + // This field is intentionally left private. + // See: https://github.com/tauri-apps/plugins-workspace/pull/3115. + code: Option, +} + +impl ExitStatus { + /// Returns the exit code of the process, if any. + pub fn code(&self) -> Option { + self.code + } + + /// Returns true if exit status is zero. Signal termination is not considered a success, and success is defined as a zero exit status. + pub fn success(&self) -> bool { + self.code == Some(0) + } +} + +/// The output of a finished process. +#[derive(Debug)] +pub struct Output { + /// The status (exit code) of the process. + pub status: ExitStatus, + /// The data that the process wrote to stdout. + pub stdout: Vec, + /// The data that the process wrote to stderr. + pub stderr: Vec, +} + +fn relative_command_path(command: &Path) -> crate::Result { + let exe_path = platform::current_exe()?; + + let exe_dir = exe_path + .parent() + .ok_or(crate::Error::CurrentExeHasNoParent)?; + + // If a test is being run, the executable is in the "deps" directory, so we need to go up one level. + let base_dir = if exe_dir.ends_with("deps") { + exe_dir.parent().unwrap_or(exe_dir) + } else { + exe_dir + }; + + let mut command_path = base_dir.join(command); + + #[cfg(windows)] + { + let already_exe = command_path.extension().is_some_and(|ext| ext == "exe"); + if !already_exe { + // do not use with_extension to retain dots in the command filename + command_path.as_mut_os_string().push(".exe"); + } + } + + #[cfg(not(windows))] + { + if command_path.extension().is_some_and(|ext| ext == "exe") { + command_path.set_extension(""); + } + } + + Ok(command_path) +} + +impl From for StdCommand { + fn from(cmd: Command) -> StdCommand { + cmd.cmd + } +} + +impl Command { + pub(crate) fn new>(program: S) -> Self { + log::debug!( + "Creating sidecar {}", + program.as_ref().to_str().unwrap_or("") + ); + let mut command = StdCommand::new(program); + + command.stdout(Stdio::piped()); + command.stdin(Stdio::piped()); + command.stderr(Stdio::piped()); + #[cfg(windows)] + command.creation_flags(CREATE_NO_WINDOW); + + Self { + cmd: command, + raw_out: false, + } + } + + pub(crate) fn new_sidecar>(program: S) -> crate::Result { + Ok(Self::new(relative_command_path(program.as_ref())?)) + } + + /// Appends an argument to the command. + #[must_use] + pub fn arg>(mut self, arg: S) -> Self { + self.cmd.arg(arg); + self + } + + /// Appends arguments to the command. + #[must_use] + pub fn args(mut self, args: I) -> Self + where + I: IntoIterator, + S: AsRef, + { + self.cmd.args(args); + self + } + + /// Clears the entire environment map for the child process. + #[must_use] + pub fn env_clear(mut self) -> Self { + self.cmd.env_clear(); + self + } + + /// Inserts or updates an explicit environment variable mapping. + #[must_use] + pub fn env(mut self, key: K, value: V) -> Self + where + K: AsRef, + V: AsRef, + { + self.cmd.env(key, value); + self + } + + /// Adds or updates multiple environment variable mappings. + #[must_use] + pub fn envs(mut self, envs: I) -> Self + where + I: IntoIterator, + K: AsRef, + V: AsRef, + { + self.cmd.envs(envs); + self + } + + /// Sets the working directory for the child process. + #[must_use] + pub fn current_dir>(mut self, current_dir: P) -> Self { + self.cmd.current_dir(current_dir); + self + } + + /// Configures the reader to output bytes from the child process exactly as received + pub fn set_raw_out(mut self, raw_out: bool) -> Self { + self.raw_out = raw_out; + self + } + + /// Spawns the command. + /// + /// # Examples + /// + /// ```rust,no_run + /// use tauri_plugin_shell::{process::CommandEvent, ShellExt}; + /// tauri::Builder::default() + /// .setup(|app| { + /// let handle = app.handle().clone(); + /// tauri::async_runtime::spawn(async move { + /// let (mut rx, mut child) = handle + /// .shell() + /// .command("cargo") + /// .args(["tauri", "dev"]) + /// .spawn() + /// .expect("Failed to spawn cargo"); + /// + /// let mut i = 0; + /// while let Some(event) = rx.recv().await { + /// if let CommandEvent::Stdout(line) = event { + /// println!("got: {}", String::from_utf8(line).unwrap()); + /// i += 1; + /// if i == 4 { + /// child.write("message from Rust\n".as_bytes()).unwrap(); + /// i = 0; + /// } + /// } + /// } + /// }); + /// Ok(()) + /// }); + /// ``` + /// + /// Depending on the command you spawn, it might output in a specific encoding, to parse the output lines in this case: + /// + /// ```rust,no_run + /// use tauri_plugin_shell::{process::{CommandEvent, Encoding}, ShellExt}; + /// tauri::Builder::default() + /// .setup(|app| { + /// let handle = app.handle().clone(); + /// tauri::async_runtime::spawn(async move { + /// let (mut rx, mut child) = handle + /// .shell() + /// .command("some-program") + /// .arg("some-arg") + /// .spawn() + /// .expect("Failed to spawn some-program"); + /// + /// let encoding = Encoding::for_label(b"windows-1252").unwrap(); + /// while let Some(event) = rx.recv().await { + /// if let CommandEvent::Stdout(line) = event { + /// let (decoded, _, _) = encoding.decode(&line); + /// println!("got: {decoded}"); + /// } + /// } + /// }); + /// Ok(()) + /// }); + /// ``` + pub fn spawn(self) -> crate::Result<(Receiver, CommandChild)> { + let raw = self.raw_out; + let mut command: StdCommand = self.into(); + let (stdout_reader, stdout_writer) = pipe()?; + let (stderr_reader, stderr_writer) = pipe()?; + let (stdin_reader, stdin_writer) = pipe()?; + command.stdout(stdout_writer); + command.stderr(stderr_writer); + command.stdin(stdin_reader); + + let shared_child = SharedChild::spawn(&mut command)?; + let child = Arc::new(shared_child); + let child_ = child.clone(); + let guard = Arc::new(RwLock::new(())); + + let (tx, rx) = channel(1); + + spawn_pipe_reader( + tx.clone(), + guard.clone(), + stdout_reader, + CommandEvent::Stdout, + raw, + ); + spawn_pipe_reader( + tx.clone(), + guard.clone(), + stderr_reader, + CommandEvent::Stderr, + raw, + ); + + spawn(move || { + let _ = match child_.wait() { + Ok(status) => { + let _l = guard.write().unwrap(); + block_on_task(async move { + tx.send(CommandEvent::Terminated(TerminatedPayload { + code: status.code(), + #[cfg(windows)] + signal: None, + #[cfg(unix)] + signal: status.signal(), + })) + .await + }) + } + Err(e) => { + let _l = guard.write().unwrap(); + block_on_task(async move { tx.send(CommandEvent::Error(e.to_string())).await }) + } + }; + }); + + Ok(( + rx, + CommandChild { + inner: child, + stdin_writer, + }, + )) + } + + /// Executes a command as a child process, waiting for it to finish and collecting its exit status. + /// Stdin, stdout and stderr are ignored. + /// + /// # Examples + /// ```rust,no_run + /// use tauri_plugin_shell::ShellExt; + /// tauri::Builder::default() + /// .setup(|app| { + /// let status = tauri::async_runtime::block_on(async move { app.shell().command("which").args(["ls"]).status().await.unwrap() }); + /// println!("`which` finished with status: {:?}", status.code()); + /// Ok(()) + /// }); + /// ``` + pub async fn status(self) -> crate::Result { + let (mut rx, _child) = self.spawn()?; + let mut code = None; + #[allow(clippy::collapsible_match)] + while let Some(event) = rx.recv().await { + if let CommandEvent::Terminated(payload) = event { + code = payload.code; + } + } + Ok(ExitStatus { code }) + } + + /// Executes the command as a child process, waiting for it to finish and collecting all of its output. + /// Stdin is ignored. + /// + /// # Examples + /// + /// ```rust,no_run + /// use tauri_plugin_shell::ShellExt; + /// tauri::Builder::default() + /// .setup(|app| { + /// let output = tauri::async_runtime::block_on(async move { app.shell().command("echo").args(["TAURI"]).output().await.unwrap() }); + /// assert!(output.status.success()); + /// assert_eq!(String::from_utf8(output.stdout).unwrap(), "TAURI"); + /// Ok(()) + /// }); + /// ``` + pub async fn output(self) -> crate::Result { + let (mut rx, _child) = self.spawn()?; + + let mut code = None; + let mut stdout = Vec::new(); + let mut stderr = Vec::new(); + + while let Some(event) = rx.recv().await { + match event { + CommandEvent::Terminated(payload) => { + code = payload.code; + } + CommandEvent::Stdout(line) => { + stdout.extend(line); + stdout.push(NEWLINE_BYTE); + } + CommandEvent::Stderr(line) => { + stderr.extend(line); + stderr.push(NEWLINE_BYTE); + } + CommandEvent::Error(_) => {} + } + } + Ok(Output { + status: ExitStatus { code }, + stdout, + stderr, + }) + } +} + +fn read_raw_bytes) -> CommandEvent + Send + Copy + 'static>( + mut reader: BufReader, + tx: Sender, + wrapper: F, +) { + loop { + let result = reader.fill_buf(); + match result { + Ok(buf) => { + let length = buf.len(); + if length == 0 { + break; + } + let tx_ = tx.clone(); + let _ = block_on_task(async move { tx_.send(wrapper(buf.to_vec())).await }); + reader.consume(length); + } + Err(e) => { + let tx_ = tx.clone(); + let _ = block_on_task( + async move { tx_.send(CommandEvent::Error(e.to_string())).await }, + ); + } + } + } +} + +fn read_line) -> CommandEvent + Send + Copy + 'static>( + mut reader: BufReader, + tx: Sender, + wrapper: F, +) { + loop { + let mut buf = Vec::new(); + match tauri::utils::io::read_line(&mut reader, &mut buf) { + Ok(n) => { + if n == 0 { + break; + } + let tx_ = tx.clone(); + let _ = block_on_task(async move { tx_.send(wrapper(buf)).await }); + } + Err(e) => { + let _ = + block_on_task(async move { tx.send(CommandEvent::Error(e.to_string())).await }); + break; + } + } + } +} + +fn spawn_pipe_reader) -> CommandEvent + Send + Copy + 'static>( + tx: Sender, + guard: Arc>, + pipe_reader: PipeReader, + wrapper: F, + raw_out: bool, +) { + spawn(move || { + let _lock = guard.read().unwrap(); + let reader = BufReader::new(pipe_reader); + + if raw_out { + read_raw_bytes(reader, tx, wrapper); + } else { + read_line(reader, tx, wrapper); + } + }); +} + +// tests for the commands functions. +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn relative_command_path_resolves() { + let cwd_parent = platform::current_exe() + .unwrap() + .parent() + .unwrap() + .parent() // Go up once more to get out of the "deps" directory + .unwrap() + .to_owned(); + assert_eq!( + relative_command_path(Path::new("Tauri.Example")).unwrap(), + cwd_parent.join(if cfg!(windows) { + "Tauri.Example.exe" + } else { + "Tauri.Example" + }) + ); + assert_eq!( + relative_command_path(Path::new("Tauri.Example.exe")).unwrap(), + cwd_parent.join(if cfg!(windows) { + "Tauri.Example.exe" + } else { + "Tauri.Example" + }) + ); + } + + #[cfg(not(windows))] + #[test] + fn test_cmd_spawn_output() { + let cmd = Command::new("cat").args(["test/test.txt"]); + let (mut rx, _) = cmd.spawn().unwrap(); + + tauri::async_runtime::block_on(async move { + while let Some(event) = rx.recv().await { + match event { + CommandEvent::Terminated(payload) => { + assert_eq!(payload.code, Some(0)); + } + CommandEvent::Stdout(line) => { + assert_eq!(String::from_utf8(line).unwrap(), "This is a test doc!"); + } + _ => {} + } + } + }); + } + + #[cfg(not(windows))] + #[test] + fn test_cmd_spawn_raw_output() { + let cmd = Command::new("cat").args(["test/test.txt"]); + let (mut rx, _) = cmd.spawn().unwrap(); + + tauri::async_runtime::block_on(async move { + while let Some(event) = rx.recv().await { + match event { + CommandEvent::Terminated(payload) => { + assert_eq!(payload.code, Some(0)); + } + CommandEvent::Stdout(line) => { + assert_eq!(String::from_utf8(line).unwrap(), "This is a test doc!"); + } + _ => {} + } + } + }); + } + + #[cfg(not(windows))] + #[test] + // test the failure case + fn test_cmd_spawn_fail() { + let cmd = Command::new("cat").args(["test/"]); + let (mut rx, _) = cmd.spawn().unwrap(); + + tauri::async_runtime::block_on(async move { + while let Some(event) = rx.recv().await { + match event { + CommandEvent::Terminated(payload) => { + assert_eq!(payload.code, Some(1)); + } + CommandEvent::Stderr(line) => { + assert_eq!( + String::from_utf8(line).unwrap(), + "cat: test/: Is a directory\n" + ); + } + _ => {} + } + } + }); + } + + #[cfg(not(windows))] + #[test] + // test the failure case (raw encoding) + fn test_cmd_spawn_raw_fail() { + let cmd = Command::new("cat").args(["test/"]); + let (mut rx, _) = cmd.spawn().unwrap(); + + tauri::async_runtime::block_on(async move { + while let Some(event) = rx.recv().await { + match event { + CommandEvent::Terminated(payload) => { + assert_eq!(payload.code, Some(1)); + } + CommandEvent::Stderr(line) => { + assert_eq!( + String::from_utf8(line).unwrap(), + "cat: test/: Is a directory\n" + ); + } + _ => {} + } + } + }); + } + + #[cfg(not(windows))] + #[test] + fn test_cmd_output_output() { + let cmd = Command::new("cat").args(["test/test.txt"]); + let output = tauri::async_runtime::block_on(cmd.output()).unwrap(); + + assert_eq!(String::from_utf8(output.stderr).unwrap(), ""); + assert_eq!( + String::from_utf8(output.stdout).unwrap(), + "This is a test doc!\n" + ); + } + + #[cfg(not(windows))] + #[test] + fn test_cmd_output_output_fail() { + let cmd = Command::new("cat").args(["test/"]); + let output = tauri::async_runtime::block_on(cmd.output()).unwrap(); + + assert_eq!(String::from_utf8(output.stdout).unwrap(), ""); + assert_eq!( + String::from_utf8(output.stderr).unwrap(), + "cat: test/: Is a directory\n\n" + ); + } +} diff --git a/plugins/shell/src/scope.rs b/plugins/shell/src/scope.rs new file mode 100644 index 0000000..35fdeaf --- /dev/null +++ b/plugins/shell/src/scope.rs @@ -0,0 +1,322 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use std::sync::Arc; + +#[allow(deprecated)] +use crate::open::Program; +use crate::process::Command; + +use regex::Regex; +use tauri::ipc::ScopeObject; +use tauri::Manager; + +/// Allowed representation of `Execute` command arguments. +#[derive(Debug, Clone, serde::Deserialize)] +#[serde(untagged, deny_unknown_fields)] +#[non_exhaustive] +pub enum ExecuteArgs { + /// No arguments + None, + + /// A single string argument + Single(String), + + /// Multiple string arguments + List(Vec), +} + +impl ExecuteArgs { + /// Whether the argument list is empty or not. + pub fn is_empty(&self) -> bool { + match self { + Self::None => true, + Self::Single(s) if s.is_empty() => true, + Self::List(l) => l.is_empty(), + _ => false, + } + } +} + +impl From<()> for ExecuteArgs { + fn from(_: ()) -> Self { + Self::None + } +} + +impl From for ExecuteArgs { + fn from(string: String) -> Self { + Self::Single(string) + } +} + +impl From> for ExecuteArgs { + fn from(vec: Vec) -> Self { + Self::List(vec) + } +} + +/// A configured scoped shell command. +#[derive(Debug, Clone)] +pub struct ScopeAllowedCommand { + /// Name of the command (key). + pub name: String, + + /// The shell command to be called. + pub command: std::path::PathBuf, + + /// The arguments the command is allowed to be called with. + pub args: Option>, + + /// If this command is a sidecar command. + pub sidecar: bool, +} + +impl ScopeObject for ScopeAllowedCommand { + type Error = crate::Error; + fn deserialize( + app: &tauri::AppHandle, + raw: tauri::utils::acl::Value, + ) -> Result { + let scope = serde_json::from_value::(raw.into())?; + + let args = match scope.args.clone() { + crate::scope_entry::ShellAllowedArgs::Flag(true) => None, + crate::scope_entry::ShellAllowedArgs::Flag(false) => Some(Vec::new()), + crate::scope_entry::ShellAllowedArgs::List(list) => { + let list = list.into_iter().map(|arg| match arg { + crate::scope_entry::ShellAllowedArg::Fixed(fixed) => { + crate::scope::ScopeAllowedArg::Fixed(fixed) + } + crate::scope_entry::ShellAllowedArg::Var { validator, raw } => { + let regex = if raw { + validator + } else { + format!("^{validator}$") + }; + let validator = Regex::new(®ex) + .unwrap_or_else(|e| panic!("invalid regex {regex}: {e}")); + crate::scope::ScopeAllowedArg::Var { validator } + } + }); + Some(list.collect()) + } + }; + + let command = if let Ok(path) = app.path().parse(&scope.command) { + path + } else { + scope.command.clone() + }; + + Ok(Self { + name: scope.name, + command, + args, + sidecar: scope.sidecar, + }) + } +} + +/// A configured argument to a scoped shell command. +#[derive(Debug, Clone)] +pub enum ScopeAllowedArg { + /// A non-configurable argument. + Fixed(String), + + /// An argument with a value to be evaluated at runtime, must pass a regex validation. + Var { + /// The validation that the variable value must pass in order to be called. + validator: Regex, + }, +} + +impl ScopeAllowedArg { + /// If the argument is fixed. + pub fn is_fixed(&self) -> bool { + matches!(self, Self::Fixed(_)) + } +} + +/// Scope for the open command +pub struct OpenScope { + /// The validation regex that `shell > open` paths must match against. + /// When set to `None`, no values are accepted. + pub open: Option, +} + +/// Scope for shell process spawning. +#[derive(Clone)] +pub struct ShellScope<'a> { + /// All allowed commands, using their unique command name as the keys. + pub scopes: Vec<&'a Arc>, +} + +/// All errors that can happen while validating a scoped command. +#[derive(Debug, thiserror::Error)] +pub enum Error { + /// At least one argument did not pass input validation. + #[error("The scoped command was called with the improper sidecar flag set")] + BadSidecarFlag, + + /// The sidecar program validated but failed to find the sidecar path. + #[error( + "The scoped sidecar command was validated, but failed to create the path to the command: {0}" + )] + Sidecar(String), + + /// The named command was not found in the scoped config. + #[error("Scoped command {0} not found")] + NotFound(String), + + /// A command variable has no value set in the arguments. + #[error( + "Scoped command argument at position {0} must match regex validation {1} but it was not found" + )] + MissingVar(usize, String), + + /// At least one argument did not pass input validation. + #[error("Scoped command argument at position {index} was found, but failed regex validation {validation}")] + Validation { + /// Index of the variable. + index: usize, + + /// Regex that the variable value failed to match. + validation: String, + }, + + /// The format of the passed input does not match the expected shape. + /// + /// This can happen from passing a string or array of strings to a command that is expecting + /// named variables, and vice-versa. + #[error("Scoped command {0} received arguments in an unexpected format")] + InvalidInput(String), + + /// A generic IO error that occurs while executing specified shell commands. + #[error("Scoped shell IO error: {0}")] + Io(#[from] std::io::Error), +} + +impl OpenScope { + /// Open a path in the default (or specified) browser. + /// + /// The path is validated against the `plugins > shell > open` validation regex, which + /// defaults to `^((mailto:\w+)|(tel:\w+)|(https?://\w+)).+`. + #[allow(deprecated)] + pub fn open(&self, path: &str, with: Option) -> Result<(), Error> { + // ensure we pass validation if the configuration has one + if let Some(regex) = &self.open { + if !regex.is_match(path) { + return Err(Error::Validation { + index: 0, + validation: regex.as_str().into(), + }); + } + } else { + log::warn!("open() command called but the plugin configuration denies calls from JavaScript; set `tauri.conf.json > plugins > shell > open` to true or a validation regex string"); + return Err(Error::Validation { + index: 0, + validation: "tauri^".to_string(), // purposefully impossible regex + }); + } + + // The prevention of argument escaping is handled by the usage of std::process::Command::arg by + // the `open` dependency. This behavior should be re-confirmed during upgrades of `open`. + match with.map(Program::name) { + Some(program) => ::open::with_detached(path, program), + None => ::open::that_detached(path), + } + .map_err(Into::into) + } +} + +impl ShellScope<'_> { + /// Validates argument inputs and creates a Tauri sidecar [`Command`]. + pub fn prepare_sidecar( + &self, + command_name: &str, + command_script: &str, + args: ExecuteArgs, + ) -> Result { + self._prepare(command_name, args, Some(command_script)) + } + + /// Validates argument inputs and creates a Tauri [`Command`]. + pub fn prepare(&self, command_name: &str, args: ExecuteArgs) -> Result { + self._prepare(command_name, args, None) + } + + /// Validates argument inputs and creates a Tauri [`Command`]. + pub fn _prepare( + &self, + command_name: &str, + args: ExecuteArgs, + sidecar: Option<&str>, + ) -> Result { + let command = match self.scopes.iter().find(|s| s.name == command_name) { + Some(command) => command, + None => return Err(Error::NotFound(command_name.into())), + }; + + if command.sidecar != sidecar.is_some() { + return Err(Error::BadSidecarFlag); + } + + let args = match (&command.args, args) { + (None, ExecuteArgs::None) => Ok(vec![]), + (None, ExecuteArgs::List(list)) => Ok(list), + (None, ExecuteArgs::Single(string)) => Ok(vec![string]), + (Some(list), ExecuteArgs::List(args)) => list + .iter() + .enumerate() + .map(|(i, arg)| match arg { + ScopeAllowedArg::Fixed(fixed) => Ok(fixed.to_string()), + ScopeAllowedArg::Var { validator } => { + let value = args + .get(i) + .ok_or_else(|| Error::MissingVar(i, validator.to_string()))? + .to_string(); + if validator.is_match(&value) { + Ok(value) + } else { + Err(Error::Validation { + index: i, + validation: validator.to_string(), + }) + } + } + }) + .collect(), + (Some(list), arg) if arg.is_empty() && list.iter().all(ScopeAllowedArg::is_fixed) => { + list.iter() + .map(|arg| match arg { + ScopeAllowedArg::Fixed(fixed) => Ok(fixed.to_string()), + _ => unreachable!(), + }) + .collect() + } + (Some(list), _) if list.is_empty() => Err(Error::InvalidInput(command_name.into())), + (Some(_), _) => Err(Error::InvalidInput(command_name.into())), + }?; + + let command_s = sidecar + .map(|s| { + std::path::PathBuf::from(s) + .components() + .next_back() + .unwrap() + .as_os_str() + .to_string_lossy() + .into_owned() + }) + .unwrap_or_else(|| command.command.to_string_lossy().into_owned()); + let command = if command.sidecar { + Command::new_sidecar(command_s).map_err(|e| Error::Sidecar(e.to_string()))? + } else { + Command::new(command_s) + }; + + Ok(command.args(args)) + } +} diff --git a/plugins/shell/src/scope_entry.rs b/plugins/shell/src/scope_entry.rs new file mode 100644 index 0000000..8a70fff --- /dev/null +++ b/plugins/shell/src/scope_entry.rs @@ -0,0 +1,77 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use serde::{de::Error as DeError, Deserialize, Deserializer}; + +use std::path::PathBuf; + +/// A command allowed to be executed by the webview API. +#[allow(dead_code)] +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub(crate) struct Entry { + pub(crate) name: String, + pub(crate) command: PathBuf, + pub(crate) args: ShellAllowedArgs, + pub(crate) sidecar: bool, +} + +#[allow(dead_code)] +#[derive(Deserialize)] +pub(crate) struct EntryRaw { + pub(crate) name: String, + #[serde(rename = "cmd")] + pub(crate) command: Option, + #[serde(default)] + pub(crate) args: ShellAllowedArgs, + #[serde(default)] + pub(crate) sidecar: bool, +} + +impl<'de> Deserialize<'de> for Entry { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let config = EntryRaw::deserialize(deserializer)?; + + if !config.sidecar && config.command.is_none() { + return Err(DeError::custom( + "The shell scope `command` value is required.", + )); + } + + Ok(Entry { + name: config.name, + command: config.command.unwrap_or_default(), + args: config.args, + sidecar: config.sidecar, + }) + } +} + +#[derive(Debug, PartialEq, Eq, Clone, Hash, Deserialize)] +#[serde(untagged, deny_unknown_fields)] +#[non_exhaustive] +pub enum ShellAllowedArgs { + Flag(bool), + List(Vec), +} + +impl Default for ShellAllowedArgs { + fn default() -> Self { + Self::Flag(false) + } +} + +#[derive(Debug, PartialEq, Eq, Clone, Hash, Deserialize)] +#[serde(untagged, deny_unknown_fields)] +#[non_exhaustive] +pub enum ShellAllowedArg { + Fixed(String), + Var { + validator: String, + #[serde(default)] + raw: bool, + }, +} diff --git a/plugins/shell/test/test.txt b/plugins/shell/test/test.txt new file mode 100644 index 0000000..ec77307 --- /dev/null +++ b/plugins/shell/test/test.txt @@ -0,0 +1 @@ +This is a test doc! \ No newline at end of file diff --git a/plugins/shell/tsconfig.json b/plugins/shell/tsconfig.json new file mode 100644 index 0000000..4374468 --- /dev/null +++ b/plugins/shell/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "rootDir": "guest-js" + }, + "include": ["guest-js/*.ts"] +} diff --git a/plugins/single-instance/CHANGELOG.md b/plugins/single-instance/CHANGELOG.md new file mode 100644 index 0000000..3e4c94a --- /dev/null +++ b/plugins/single-instance/CHANGELOG.md @@ -0,0 +1,268 @@ +# Changelog + +## [2.4.5] + +- [`67cd25a1`](https://github.com/tauri-apps/plugins-workspace/commit/67cd25a10ce3deb1b935e725228e528766f24ad7) ([#3592](https://github.com/tauri-apps/plugins-workspace/pull/3592)) On Windows, the second instance now allows the first instance to bring its window to the front before exiting, so focusing a window from the callback no longer gets refused by Windows. + +## [2.4.4] + +- [`f8053e65`](https://github.com/tauri-apps/plugins-workspace/commit/f8053e659e4ccd85c1f52833411ff8417cbc5e69) ([#3527](https://github.com/tauri-apps/plugins-workspace/pull/3527) by [@Legend-Master](https://github.com/tauri-apps/plugins-workspace/../../Legend-Master)) Documented Cargo feature flags in each plugin's crate-level documentation. + +### Dependencies + +- Upgraded to `deep-link@2.4.10` + +## \[2.4.3] + +- [`d1573877`](https://github.com/tauri-apps/plugins-workspace/commit/d1573877226e609461761aa538cd0ca4f24d22be) ([#3466](https://github.com/tauri-apps/plugins-workspace/pull/3466) by [@bajoca05](https://github.com/tauri-apps/plugins-workspace/../../bajoca05)) Fix blocked thread on the single-instance plugin for MacOS: replace standard `UnixListener` with `tokio::net::UnixListener`, so the task can yield. + +## \[2.4.2] + +### Dependencies + +- Upgraded to `deep-link@2.4.9` + +## \[2.4.1] + +### Dependencies + +- Upgraded to `deep-link@2.4.8` + +## \[2.4.0] + +### Dependencies + +- Upgraded to `deep-link@2.4.7` + +### fix + +- [`98e2c11e`](https://github.com/tauri-apps/plugins-workspace/commit/98e2c11eefc3ee562f1ed280efe7e8ea6ff0f3b0) ([#3194](https://github.com/tauri-apps/plugins-workspace/pull/3194) by [@mrquantumoff](https://github.com/tauri-apps/plugins-workspace/../../mrquantumoff)) **Breaking Change:** On Linux, the DBus ID/name will now be `.SingleInstance` instead of `org..SingleInstance` to follow DBus specifications. + + This will break the single-instance mechanism across different app versions if the app was installed multiple times. + + Added `dbus_id` builder method, which can be used to restore previous behavior. For a bundle identifier of `com.tauri.my-example` this would be `dbus_id("org.com_tauri_my_example")`. + +## \[2.3.7] + +### Dependencies + +- Upgraded to `deep-link@2.4.6` + +## \[2.3.6] + +- [`93426f85`](https://github.com/tauri-apps/plugins-workspace/commit/93426f85120f49beb9f40222bff45185a32d54a9) Fixed an issue that caused docs.rs builds to fail. No user facing changes. + +### Dependencies + +- Upgraded to `deep-link@2.4.5` + +## \[2.3.5] + +### Dependencies + +- Upgraded to `deep-link@2.4.4` + +## \[2.3.4] + +### Dependencies + +- Upgraded to `deep-link@2.4.3` + +## \[2.3.3] + +### Dependencies + +- Upgraded to `deep-link@2.4.2` + +## \[2.3.2] + +### Dependencies + +- Upgraded to `deep-link@2.4.1` + +## \[2.3.1] + +- [`6f345870`](https://github.com/tauri-apps/plugins-workspace/commit/6f345870df4e7b187deb869df03b79858e03b4fe) ([#2860](https://github.com/tauri-apps/plugins-workspace/pull/2860) by [@MorpheusXAUT](https://github.com/tauri-apps/plugins-workspace/../../MorpheusXAUT)) Fix D-Bus name replacement logic on Linux to prevent multiple instances from acquiring the same well-known name.\ + This patch updates the `zbus` dependency to the latest compatible version (`^5.9`) and explicitly sets `RequestNameFlags` to ensure a second instance fails to acquire the D-Bus name when another one is already running. + +## \[2.3.0] + +- [`f209b2f2`](https://github.com/tauri-apps/plugins-workspace/commit/f209b2f23cb29133c97ad5961fb46ef794dbe063) ([#2804](https://github.com/tauri-apps/plugins-workspace/pull/2804) by [@renovate](https://github.com/tauri-apps/plugins-workspace/../../renovate)) Updated tauri to 2.6 + +### Dependencies + +- Upgraded to `deep-link@2.4.0` + +## \[2.2.4] + +- [`dc84f8d8`](https://github.com/tauri-apps/plugins-workspace/commit/dc84f8d8bbaa70de3bb3185fbacb472993b996ef) ([#2609](https://github.com/tauri-apps/plugins-workspace/pull/2609) by [@Simon-Laux](https://github.com/tauri-apps/plugins-workspace/../../Simon-Laux)) fix `cwd` in single instance on macOS, which was the cwd of the first instance, instead of the second (like it is on windows and linux) + +### Dependencies + +- Upgraded to `deep-link@2.3.0` + +## \[2.2.3] + +### Dependencies + +- Upgraded to `deep-link@2.2.1` + +## \[2.2.2] + +- [`1ab5f157`](https://github.com/tauri-apps/plugins-workspace/commit/1ab5f1576333174095bc7dad4bef7a8576bb29ab) ([#2452](https://github.com/tauri-apps/plugins-workspace/pull/2452) by [@FabianLars](https://github.com/tauri-apps/plugins-workspace/../../FabianLars)) Fixed `null pointer dereference` panic on rust nightly on Windows. + +## \[2.2.1] + +- [`da5c59e2`](https://github.com/tauri-apps/plugins-workspace/commit/da5c59e2fe879d177e3cfd52fcacce85440423cb) ([#2271](https://github.com/tauri-apps/plugins-workspace/pull/2271) by [@renovate](https://github.com/tauri-apps/plugins-workspace/../../renovate)) Updated `zbus` dependency to version 5. No API changes. + +## \[2.2.0] + +- [`3a79266b`](https://github.com/tauri-apps/plugins-workspace/commit/3a79266b8cf96a55b1ae6339d725567d45a44b1d) ([#2173](https://github.com/tauri-apps/plugins-workspace/pull/2173) by [@FabianLars](https://github.com/tauri-apps/plugins-workspace/../../FabianLars)) Bumped all plugins to `v2.2.0`. From now, the versions for the Rust and JavaScript packages of each plugin will be in sync with each other. + +### Dependencies + +- Upgraded to `deep-link@2.1.0` + +## \[2.0.2] + +### Dependencies + +- Upgraded to `deep-link@2.0.2` + +## \[2.0.1] + +- [`a1a82208`](https://github.com/tauri-apps/plugins-workspace/commit/a1a82208ed4ab87f83310be0dc95428aec9ab241) ([#1873](https://github.com/tauri-apps/plugins-workspace/pull/1873) by [@lucasfernog](https://github.com/tauri-apps/plugins-workspace/../../lucasfernog)) Downgrade MSRV to 1.77.2 to support Windows 7. + +### Dependencies + +- Upgraded to `deep-link@2.0.1` + +## \[2.0.0] + +- [`e2c4dfb6`](https://github.com/tauri-apps/plugins-workspace/commit/e2c4dfb6af43e5dd8d9ceba232c315f5febd55c1) Update to tauri v2 stable release. + +### Dependencies + +- Upgraded to `deep-link@2.0.0` + +## \[2.0.0-rc.5] + +### Dependencies + +- Upgraded to `deep-link@2.0.0-rc.7` + +## \[2.0.0-rc.4] + +### Dependencies + +- Upgraded to `deep-link@2.0.0-rc.6` + +## \[2.0.0-rc.3] + +- [`b2269333`](https://github.com/tauri-apps/plugins-workspace/commit/b2269333e39afe32629a11763a8e25d0b12b132b) ([#1766](https://github.com/tauri-apps/plugins-workspace/pull/1766) by [@Legend-Master](https://github.com/tauri-apps/plugins-workspace/../../Legend-Master)) Put deep link integration behined a feature + +### Dependencies + +- Upgraded to `deep-link@2.0.0-rc.5` + +## \[2.0.0-rc.2] + +- [`64a6240f`](https://github.com/tauri-apps/plugins-workspace/commit/64a6240f79fcd52267c8d721b727ae695055d7ff) ([#1759](https://github.com/tauri-apps/plugins-workspace/pull/1759) by [@lucasfernog](https://github.com/tauri-apps/plugins-workspace/../../lucasfernog)) Integrate with the deep link plugin out of the box. + +### Dependencies + +- Upgraded to `deep-link@2.0.0-rc.4` + +## \[2.0.0-rc.1] + +- [`3c52f30e`](https://github.com/tauri-apps/plugins-workspace/commit/3c52f30ea4ec29c51f7021aa7871614d72e43258) ([#1665](https://github.com/tauri-apps/plugins-workspace/pull/1665) by [@amrbashir](https://github.com/tauri-apps/plugins-workspace/../../amrbashir)) Updated `windows-sys` crate to `0.59` +- [`e2e97db5`](https://github.com/tauri-apps/plugins-workspace/commit/e2e97db51983267f5be84d4f6f0278d58834d1f5) ([#1701](https://github.com/tauri-apps/plugins-workspace/pull/1701) by [@lucasfernog](https://github.com/tauri-apps/plugins-workspace/../../lucasfernog)) Update to tauri 2.0.0-rc.8 + +## \[2.0.0-rc.0] + +- [`9887d1`](https://github.com/tauri-apps/plugins-workspace/commit/9887d14bd0e971c4c0f5c1188fc4005d3fc2e29e) Update to tauri RC. + +## \[2.0.0-beta.12] + +- [`e847cedc`](https://github.com/tauri-apps/plugins-workspace/commit/e847cedc1f46f3e7a2ad81ea579b620bc5b992d7) ([#1402](https://github.com/tauri-apps/plugins-workspace/pull/1402) by [@Legend-Master](https://github.com/tauri-apps/plugins-workspace/../../Legend-Master)) Use no default features on tauri for all plugins so that consumers can use `default-features = false` on tauri, note that this will still enable wry feature on iOS +- [`6de87966`](https://github.com/tauri-apps/plugins-workspace/commit/6de87966ecc00ad9d91c25be452f1f46bd2b7e1f) ([#1597](https://github.com/tauri-apps/plugins-workspace/pull/1597) by [@Legend-Master](https://github.com/tauri-apps/plugins-workspace/../../Legend-Master)) Update to tauri beta.25. + +## \[2.0.0-beta.11] + +- [`22a17980`](https://github.com/tauri-apps/plugins-workspace/commit/22a17980ff4f6f8c40adb1b8f4ffc6dae2fe7e30) ([#1537](https://github.com/tauri-apps/plugins-workspace/pull/1537) by [@lucasfernog](https://github.com/tauri-apps/plugins-workspace/../../lucasfernog)) Update to tauri beta.24. + +## \[2.0.0-beta.10] + +- [`76daee7a`](https://github.com/tauri-apps/plugins-workspace/commit/76daee7aafece34de3092c86e531cf9eb1138989) ([#1512](https://github.com/tauri-apps/plugins-workspace/pull/1512) by [@renovate](https://github.com/tauri-apps/plugins-workspace/../../renovate)) Update to tauri beta.23. + +## \[2.0.0-beta.9] + +- [`9013854f`](https://github.com/tauri-apps/plugins-workspace/commit/9013854f42a49a230b9dbb9d02774765528a923f)([#1382](https://github.com/tauri-apps/plugins-workspace/pull/1382)) Update to tauri beta.22. + +## \[2.0.0-beta.8] + +- [`430bd6f4`](https://github.com/tauri-apps/plugins-workspace/commit/430bd6f4f379bee5d232ae6b098ae131db7f178a)([#1363](https://github.com/tauri-apps/plugins-workspace/pull/1363)) Update to tauri beta.20. + +## \[2.0.0-beta.7] + +- [`bd1ed590`](https://github.com/tauri-apps/plugins-workspace/commit/bd1ed5903ffcce5500310dac1e59e8c67674ef1e)([#1237](https://github.com/tauri-apps/plugins-workspace/pull/1237)) Update to tauri beta.17. + +## \[2.0.0-beta.6] + +- [`ed46dca`](https://github.com/tauri-apps/plugins-workspace/commit/ed46dca74ff3947dbbcb26a7b571c129bf925698) Added the `semver` feature flag to make the single instance mechanism only trigger for semver compatible versions. + +## \[2.0.0-beta.5] + +- [`dabac0e`](https://github.com/tauri-apps/plugins-workspace/commit/dabac0eedfd6e6d192c6c5a214e708b3c0223f6f)([#1035](https://github.com/tauri-apps/plugins-workspace/pull/1035)) Added implementation for MacOS. + +## \[2.0.0-beta.4] + +- [`a04ea2f`](https://github.com/tauri-apps/plugins-workspace/commit/a04ea2f38294d5a3987578283badc8eec87a7752)([#1071](https://github.com/tauri-apps/plugins-workspace/pull/1071)) The global API script is now only added to the binary when the `withGlobalTauri` config is true. + +## \[2.0.0-beta.3] + +- [`2397ec5`](https://github.com/tauri-apps/plugins-workspace/commit/2397ec5937e594397e533925ccd257cae30b4cd1)([#1019](https://github.com/tauri-apps/plugins-workspace/pull/1019)) Fix doesn't shutdown immediately. +- [`99bea25`](https://github.com/tauri-apps/plugins-workspace/commit/99bea2559c2c0648c2519c50a18cd124dacef57b)([#1005](https://github.com/tauri-apps/plugins-workspace/pull/1005)) Update to tauri beta.8. + +## \[2.0.0-beta.2] + +- [`6d1e621`](https://github.com/tauri-apps/plugins-workspace/commit/6d1e6218b5877ef91f589f790f7251acda9c9605)([#981](https://github.com/tauri-apps/plugins-workspace/pull/981)) Fix `zbus::blocking::connection::Builder` import. + +## \[2.0.0-beta.1] + +- [`14f381a`](https://github.com/tauri-apps/plugins-workspace/commit/14f381acf8fe690acecc676922c6f05939b95734) Update MSRV to 1.75. +- [`569defb`](https://github.com/tauri-apps/plugins-workspace/commit/569defbe9492e38938554bb7bdc1be9151456d21) Update to tauri beta.4. + +## \[2.0.0-beta.0] + +- [`d198c01`](https://github.com/tauri-apps/plugins-workspace/commit/d198c014863ee260cb0de88a14b7fc4356ef7474)([#862](https://github.com/tauri-apps/plugins-workspace/pull/862)) Update to tauri beta. +- [`d198c01`](https://github.com/tauri-apps/plugins-workspace/commit/d198c014863ee260cb0de88a14b7fc4356ef7474)([#862](https://github.com/tauri-apps/plugins-workspace/pull/862)) Add permissions. + +## \[2.0.0-alpha.6] + +- [`2cf8faa`](https://github.com/tauri-apps/plugins-workspace/commit/2cf8faa3e149af55eb86e5aba8ebfc54210ca703)([#839](https://github.com/tauri-apps/plugins-workspace/pull/839)) Update to tauri@alpha.20. + +## \[2.0.0-alpha.5] + +- [`387c2f9`](https://github.com/tauri-apps/plugins-workspace/commit/387c2f9e0ce4c75c07ffa3fd76391a25b58f5daf)([#802](https://github.com/tauri-apps/plugins-workspace/pull/802)) Update to tauri@alpha.18. + +## \[2.0.0-alpha.4] + +- [`e438e0a`](https://github.com/tauri-apps/plugins-workspace/commit/e438e0a62d4b430a5159f05f13ecd397dd891a0d)([#676](https://github.com/tauri-apps/plugins-workspace/pull/676)) Update to tauri@alpha.17. + +## \[2.0.0-alpha.3] + +- [`5c13736`](https://github.com/tauri-apps/plugins-workspace/commit/5c137365c60790e8d4037d449e8237aa3fffdab0)([#673](https://github.com/tauri-apps/plugins-workspace/pull/673)) Update MSRV to 1.75. + +## \[2.0.0-alpha.2] + +- [`4e2cef9`](https://github.com/tauri-apps/plugins-workspace/commit/4e2cef9b702bbbb9cf4ee17de50791cb21f1b2a4)([#593](https://github.com/tauri-apps/plugins-workspace/pull/593)) Update to alpha.12. + +## \[2.0.0-alpha.1] + +- [`d5a7c77`](https://github.com/tauri-apps/plugins-workspace/commit/d5a7c77a8d0e7912a6b07b22ed329004edd6e80b)([#545](https://github.com/tauri-apps/plugins-workspace/pull/545)) Fixes docs.rs build by enabling the `tauri/dox` feature flag. +- [`d74fc0a`](https://github.com/tauri-apps/plugins-workspace/commit/d74fc0a097996e90a37be8f57d50b7d1f6ca616f)([#555](https://github.com/tauri-apps/plugins-workspace/pull/555)) Update to alpha.11. + +## \[2.0.0-alpha.0] + +- [`717ae67`](https://github.com/tauri-apps/plugins-workspace/commit/717ae670978feb4492fac1f295998b93f2b9347f)([#371](https://github.com/tauri-apps/plugins-workspace/pull/371)) First v2 alpha release! diff --git a/plugins/single-instance/Cargo.toml b/plugins/single-instance/Cargo.toml new file mode 100644 index 0000000..e636a7f --- /dev/null +++ b/plugins/single-instance/Cargo.toml @@ -0,0 +1,46 @@ +[package] +name = "tauri-plugin-single-instance" +version = "2.4.5" +description = "Ensure a single instance of your tauri app is running." +authors = { workspace = true } +license = { workspace = true } +edition = { workspace = true } +rust-version = { workspace = true } +repository = { workspace = true } +exclude = ["/banner.png", "/examples"] + +[package.metadata.platforms.support] +windows = { level = "full", notes = "" } +linux = { level = "full", notes = "" } +macos = { level = "full", notes = "" } +android = { level = "none", notes = "" } +ios = { level = "none", notes = "" } + +[dependencies] +serde = { workspace = true } +serde_json = { workspace = true } +tauri = { workspace = true } +tracing = { workspace = true } +thiserror = { workspace = true } +tauri-plugin-deep-link = { path = "../deep-link", version = "2.4.10", optional = true } +semver = { version = "1", optional = true } +tokio = { version = "1", features = ["net"] } + +[target."cfg(target_os = \"windows\")".dependencies.windows-sys] +version = "0.60" +features = [ + "Win32_System_Threading", + "Win32_System_DataExchange", + "Win32_Foundation", + "Win32_UI_WindowsAndMessaging", + "Win32_Security", + "Win32_System_LibraryLoader", + "Win32_Graphics_Gdi", +] + +[target."cfg(target_os = \"linux\")".dependencies] +zbus = { workspace = true } + +[features] +semver = ["dep:semver"] +deep-link = ["dep:tauri-plugin-deep-link"] diff --git a/plugins/single-instance/LICENSE.spdx b/plugins/single-instance/LICENSE.spdx new file mode 100644 index 0000000..cdd0df5 --- /dev/null +++ b/plugins/single-instance/LICENSE.spdx @@ -0,0 +1,20 @@ +SPDXVersion: SPDX-2.1 +DataLicense: CC0-1.0 +PackageName: tauri +DataFormat: SPDXRef-1 +PackageSupplier: Organization: The Tauri Programme in the Commons Conservancy +PackageHomePage: https://tauri.app +PackageLicenseDeclared: Apache-2.0 +PackageLicenseDeclared: MIT +PackageCopyrightText: 2019-2022, The Tauri Programme in the Commons Conservancy +PackageSummary: Tauri is a rust project that enables developers to make secure +and small desktop applications using a web frontend. + +PackageComment: The package includes the following libraries; see +Relationship information. + +Created: 2019-05-20T09:00:00Z +PackageDownloadLocation: git://github.com/tauri-apps/tauri +PackageDownloadLocation: git+https://github.com/tauri-apps/tauri.git +PackageDownloadLocation: git+ssh://github.com/tauri-apps/tauri.git +Creator: Person: Daniel Thompson-Yvetot \ No newline at end of file diff --git a/plugins/single-instance/LICENSE_APACHE-2.0 b/plugins/single-instance/LICENSE_APACHE-2.0 new file mode 100644 index 0000000..4947287 --- /dev/null +++ b/plugins/single-instance/LICENSE_APACHE-2.0 @@ -0,0 +1,177 @@ + + 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 \ No newline at end of file diff --git a/plugins/single-instance/LICENSE_MIT b/plugins/single-instance/LICENSE_MIT new file mode 100644 index 0000000..0840164 --- /dev/null +++ b/plugins/single-instance/LICENSE_MIT @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2017 - Present The Tauri Programme in 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. \ No newline at end of file diff --git a/plugins/single-instance/README.md b/plugins/single-instance/README.md new file mode 100644 index 0000000..e5f1fd8 --- /dev/null +++ b/plugins/single-instance/README.md @@ -0,0 +1,90 @@ +![tauri-plugin-single-instance](https://github.com/tauri-apps/plugins-workspace/raw/v2/plugins/single-instance/banner.png) + +Ensure a single instance of your tauri app is running. + +| Platform | Supported | +| -------- | --------- | +| Linux | ✓ | +| Windows | ✓ | +| macOS | ✓ | +| Android | x | +| iOS | x | + +## Install + +_This plugin requires a Rust version of at least **1.77.2**_ + +There are three general methods of installation that we can recommend. + +1. Use crates.io and npm (easiest, and requires you to trust that our publishing pipeline worked) +2. Pull sources directly from Github using git tags / revision hashes (most secure) +3. Git submodule install this repo in your tauri project and then use file protocol to ingest the source (most secure, but inconvenient to use) + +Install the Core plugin by adding the following to your `Cargo.toml` file: + +`src-tauri/Cargo.toml` + +```toml +[dependencies] +tauri-plugin-single-instance = "2.0.0" +# alternatively with Git: +tauri-plugin-single-instance = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v2" } +``` + +## Usage + +First you need to register the core plugin with Tauri: + +`src-tauri/src/lib.rs` + +```rust +use tauri::{Manager}; + +#[derive(Clone, serde::Serialize)] +struct Payload { + args: Vec, + cwd: String, +} + +fn main() { + tauri::Builder::default() + .plugin(tauri_plugin_single_instance::init(|app, argv, cwd| { + println!("{}, {argv:?}, {cwd}", app.package_info().name); + app.emit("single-instance", Payload { args: argv, cwd }).unwrap(); + })) + .run(tauri::generate_context!()) + .expect("error while running tauri application"); +} +``` + +Note that currently, plugins run in the order they were added in to the builder, so make sure that this plugin is registered first. + +## Usage with Flatpak/Snap + +If you use Flatpak/Snap to publish your package and your Tauri identifier doesn't match the package id, set the `DBUS_ID` variable using the builder for the plugin, look at example. + +## Contributing + +PRs accepted. Please make sure to read the Contributing Guide before making a pull request. + +## Partners + + + + + + + +
+ + CrabNebula + +
+ +For the complete list of sponsors please visit our [website](https://tauri.app#sponsors) and [Open Collective](https://opencollective.com/tauri). + +## License + +Code: (c) 2015 - Present - The Tauri Programme within The Commons Conservancy. + +MIT or MIT/Apache 2.0 where applicable. diff --git a/plugins/single-instance/SECURITY.md b/plugins/single-instance/SECURITY.md new file mode 100644 index 0000000..4f09bba --- /dev/null +++ b/plugins/single-instance/SECURITY.md @@ -0,0 +1,23 @@ +# Security Policy + +**Do not report security vulnerabilities through public GitHub issues.** + +**Please use the [Private Vulnerability Disclosure](https://docs.github.com/en/code-security/security-advisories/guidance-on-reporting-and-writing-information-about-vulnerabilities/privately-reporting-a-security-vulnerability#privately-reporting-a-security-vulnerability) feature of GitHub.** + +Include as much of the following information: + +- Type of issue (e.g. improper input parsing, privilege escalation, etc.) +- The location of the affected source code (tag/branch/commit or direct URL) +- Any special configuration required to reproduce the issue +- The distribution affected or used to help us with reproduction of the issue +- Step-by-step instructions to reproduce the issue +- Ideally a reproduction repository +- Impact of the issue, including how an attacker might exploit the issue + +We prefer to receive reports in English. + +## Contact + +Please disclose a vulnerability or security relevant issue here: [https://github.com/tauri-apps/plugins-workspace/security/advisories/new](https://github.com/tauri-apps/plugins-workspace/security/advisories/new). + +Alternatively, you can also contact us by email via [security@tauri.app](mailto:security@tauri.app). diff --git a/plugins/single-instance/banner.png b/plugins/single-instance/banner.png new file mode 100644 index 0000000000000000000000000000000000000000..8e3cc5a8df4b1727d731707387d213a839873b2e GIT binary patch literal 47361 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 RKR9j6)3~d1r%VkS@_#-X+&2IK literal 0 HcmV?d00001 diff --git a/plugins/single-instance/examples/vanilla/.gitignore b/plugins/single-instance/examples/vanilla/.gitignore new file mode 100644 index 0000000..b3f41c3 --- /dev/null +++ b/plugins/single-instance/examples/vanilla/.gitignore @@ -0,0 +1,3 @@ +node_modules/ +dist/** +!dist/.gitkeep \ No newline at end of file diff --git a/plugins/single-instance/examples/vanilla/package.json b/plugins/single-instance/examples/vanilla/package.json new file mode 100644 index 0000000..d92ff0d --- /dev/null +++ b/plugins/single-instance/examples/vanilla/package.json @@ -0,0 +1,14 @@ +{ + "name": "app", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "tauri": "tauri" + }, + "author": "", + "license": "MIT", + "devDependencies": { + "@tauri-apps/cli": "2.11.4" + } +} diff --git a/plugins/single-instance/examples/vanilla/public/index.html b/plugins/single-instance/examples/vanilla/public/index.html new file mode 100644 index 0000000..9c95f40 --- /dev/null +++ b/plugins/single-instance/examples/vanilla/public/index.html @@ -0,0 +1,5 @@ + + +
Plugin example
+ + diff --git a/plugins/single-instance/examples/vanilla/src-tauri/.gitignore b/plugins/single-instance/examples/vanilla/src-tauri/.gitignore new file mode 100644 index 0000000..949785a --- /dev/null +++ b/plugins/single-instance/examples/vanilla/src-tauri/.gitignore @@ -0,0 +1,6 @@ +# Generated by Cargo +# will have compiled files and executables +/target/ +WixTools + +/gen/schemas diff --git a/plugins/single-instance/examples/vanilla/src-tauri/Cargo.toml b/plugins/single-instance/examples/vanilla/src-tauri/Cargo.toml new file mode 100644 index 0000000..c98659a --- /dev/null +++ b/plugins/single-instance/examples/vanilla/src-tauri/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "single-instance-example" +version = "0.1.0" +description = "A Tauri App" +authors = ["You"] +repository = "" +edition = "2021" +rust-version = "1.77.2" + +[dependencies] +serde = { workspace = true } +serde_json = { workspace = true } +tauri = { workspace = true, features = ["wry", "common-controls-v6", "x11"] } +tauri-plugin-single-instance = { path = "../../../" } +tauri-plugin-cli = { path = "../../../../cli" } + +[build-dependencies] +tauri-build = { workspace = true } diff --git a/plugins/single-instance/examples/vanilla/src-tauri/build.rs b/plugins/single-instance/examples/vanilla/src-tauri/build.rs new file mode 100644 index 0000000..5ebf8d2 --- /dev/null +++ b/plugins/single-instance/examples/vanilla/src-tauri/build.rs @@ -0,0 +1,7 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +fn main() { + tauri_build::build() +} diff --git a/plugins/single-instance/examples/vanilla/src-tauri/icons/128x128.png b/plugins/single-instance/examples/vanilla/src-tauri/icons/128x128.png new file mode 100644 index 0000000000000000000000000000000000000000..6be5e50e9b9ae84d9e2ee433f32ef446495eaf3b GIT binary patch literal 3512 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 literal 0 HcmV?d00001 diff --git a/plugins/single-instance/examples/vanilla/src-tauri/icons/128x128@2x.png b/plugins/single-instance/examples/vanilla/src-tauri/icons/128x128@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..e81becee571e96f76aa5667f9324c05e5e7a4479 GIT binary patch literal 7012 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 Sh@|Ye~|0>}wMPLT8jm@Y!H33Sz}5aFI6 zM9Lzqz|;A*0sGs=2A1uU!1nk2dGF7knQwr99SAFen)x(eCO;F8y2C~0FD1YxRTPcy zPWVxkUYmeuz}Tv?7&Fe-!UE{)ZW)Mb;H)^#eHDv$`dkZGguJz@^MA!ZNGAUqt{|0H zpZ7Ch9S`q5!>R%}>}62!+(T^evyO+ImSo2wpu)su4^3nw5(%)KD%gbSev^*HZZ&3( z#&c@Z0gH|}Ck)w6fh0&NBJ62ib%R}(3@$VFl*_#l2W$wQ-~4RmZZAt5O*^2Q5}Xr8Hy@c`#pM?kc?hFWxRXr*mUfUCXf4ka5DD~ zat6d85COB05l#(P9*cQZ3EC8fVdS~?&vN#rce(aF9@xp80O2{{FBvU+{X>Hoh;xI` z{$e^Nw1y*VbO8wv`8|-m?NwNaKGTGaF{P^JLB^DbOYWIbn%eT`*!^C1H36=O8Z-M> zkD~88ry`eSo`tEBN4>w7OWZwUzlh{WM1m8R6zepqGcGMaV7vWY9b?K4b6~|HVG)ec wi>I@ws#sZo7or4_*4M>7;p5{nr2pZ?Uu4>Krr0kU)&Kwi07*qoM6N<$f)&@lf&c&j literal 0 HcmV?d00001 diff --git a/plugins/single-instance/examples/vanilla/src-tauri/icons/icon.ico b/plugins/single-instance/examples/vanilla/src-tauri/icons/icon.ico new file mode 100644 index 0000000000000000000000000000000000000000..b3636e4b22ba65db9061cd60a77b02c92022dfd6 GIT binary patch literal 86642 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 AKmY&$ literal 0 HcmV?d00001 diff --git a/plugins/single-instance/examples/vanilla/src-tauri/icons/icon.png b/plugins/single-instance/examples/vanilla/src-tauri/icons/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..e1cd2619e0b5ec089cbba5ec7b03ddf2b1dfceb6 GIT binary patch literal 14183 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

` headings in markdown content for Tauri documentation generation purposes.", + "type": [ + "string", + "null" + ] + }, + "permissions": { + "description": "All permissions this set contains.", + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "PermissionSet": { + "description": "A set of direct permissions grouped together under a new name.", + "type": "object", + "required": [ + "description", + "identifier", + "permissions" + ], + "properties": { + "identifier": { + "description": "A unique identifier for the permission.", + "type": "string" + }, + "description": { + "description": "Human-readable description of what the permission does.", + "type": "string" + }, + "permissions": { + "description": "All permissions this set contains.", + "type": "array", + "items": { + "$ref": "#/definitions/PermissionKind" + } + } + } + }, + "Permission": { + "description": "Descriptions of explicit privileges of commands.\n\nIt can enable commands to be accessible in the frontend of the application.\n\nIf the scope is defined it can be used to fine grain control the access of individual or multiple commands.", + "type": "object", + "required": [ + "identifier" + ], + "properties": { + "version": { + "description": "The version of the permission.", + "type": [ + "integer", + "null" + ], + "format": "uint64", + "minimum": 1.0 + }, + "identifier": { + "description": "A unique identifier for the permission.", + "type": "string" + }, + "description": { + "description": "Human-readable description of what the permission does. Tauri internal convention is to use `

` headings in markdown content for Tauri documentation generation purposes.", + "type": [ + "string", + "null" + ] + }, + "commands": { + "description": "Allowed or denied commands when using this permission.", + "default": { + "allow": [], + "deny": [] + }, + "allOf": [ + { + "$ref": "#/definitions/Commands" + } + ] + }, + "scope": { + "description": "Allowed or denied scoped when using this permission.", + "allOf": [ + { + "$ref": "#/definitions/Scopes" + } + ] + }, + "platforms": { + "description": "Target platforms this permission applies. By default all platforms are affected by this permission.", + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/Target" + } + } + } + }, + "Commands": { + "description": "Allowed and denied commands inside a permission.\n\nIf two commands clash inside of `allow` and `deny`, it should be denied by default.", + "type": "object", + "properties": { + "allow": { + "description": "Allowed command.", + "default": [], + "type": "array", + "items": { + "type": "string" + } + }, + "deny": { + "description": "Denied command, which takes priority.", + "default": [], + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "Scopes": { + "description": "An argument for fine grained behavior control of Tauri commands.\n\nIt can be of any serde serializable type and is used to allow or prevent certain actions inside a Tauri command. The configured scope is passed to the command and will be enforced by the command implementation.\n\n## Example\n\n```json { \"allow\": [{ \"path\": \"$HOME/**\" }], \"deny\": [{ \"path\": \"$HOME/secret.txt\" }] } ```", + "type": "object", + "properties": { + "allow": { + "description": "Data that defines what is allowed by the scope.", + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/Value" + } + }, + "deny": { + "description": "Data that defines what is denied by the scope. This should be prioritized by validation logic.", + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/Value" + } + } + } + }, + "Value": { + "description": "All supported ACL values.", + "anyOf": [ + { + "description": "Represents a null JSON value.", + "type": "null" + }, + { + "description": "Represents a [`bool`].", + "type": "boolean" + }, + { + "description": "Represents a valid ACL [`Number`].", + "allOf": [ + { + "$ref": "#/definitions/Number" + } + ] + }, + { + "description": "Represents a [`String`].", + "type": "string" + }, + { + "description": "Represents a list of other [`Value`]s.", + "type": "array", + "items": { + "$ref": "#/definitions/Value" + } + }, + { + "description": "Represents a map of [`String`] keys to [`Value`]s.", + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/Value" + } + } + ] + }, + "Number": { + "description": "A valid ACL number.", + "anyOf": [ + { + "description": "Represents an [`i64`].", + "type": "integer", + "format": "int64" + }, + { + "description": "Represents a [`f64`].", + "type": "number", + "format": "double" + } + ] + }, + "Target": { + "description": "Platform target.", + "oneOf": [ + { + "description": "MacOS.", + "type": "string", + "enum": [ + "macOS" + ] + }, + { + "description": "Windows.", + "type": "string", + "enum": [ + "windows" + ] + }, + { + "description": "Linux.", + "type": "string", + "enum": [ + "linux" + ] + }, + { + "description": "Android.", + "type": "string", + "enum": [ + "android" + ] + }, + { + "description": "iOS.", + "type": "string", + "enum": [ + "iOS" + ] + } + ] + }, + "PermissionKind": { + "type": "string", + "oneOf": [ + { + "description": "Enables the close command without any pre-configured scope.", + "type": "string", + "const": "allow-close", + "markdownDescription": "Enables the close command without any pre-configured scope." + }, + { + "description": "Denies the close command without any pre-configured scope.", + "type": "string", + "const": "deny-close", + "markdownDescription": "Denies the close command without any pre-configured scope." + }, + { + "description": "Enables the execute command without any pre-configured scope.", + "type": "string", + "const": "allow-execute", + "markdownDescription": "Enables the execute command without any pre-configured scope." + }, + { + "description": "Denies the execute command without any pre-configured scope.", + "type": "string", + "const": "deny-execute", + "markdownDescription": "Denies the execute command without any pre-configured scope." + }, + { + "description": "Enables the load command without any pre-configured scope.", + "type": "string", + "const": "allow-load", + "markdownDescription": "Enables the load command without any pre-configured scope." + }, + { + "description": "Denies the load command without any pre-configured scope.", + "type": "string", + "const": "deny-load", + "markdownDescription": "Denies the load command without any pre-configured scope." + }, + { + "description": "Enables the select command without any pre-configured scope.", + "type": "string", + "const": "allow-select", + "markdownDescription": "Enables the select command without any pre-configured scope." + }, + { + "description": "Denies the select command without any pre-configured scope.", + "type": "string", + "const": "deny-select", + "markdownDescription": "Denies the select command without any pre-configured scope." + }, + { + "description": "### Default Permissions\n\nThis permission set configures what kind of\ndatabase operations are available from the sql plugin.\n\n### Granted Permissions\n\nAll reading related operations are enabled.\nAlso allows to load or close a connection.\n\n\n#### This default permission set includes:\n\n- `allow-close`\n- `allow-load`\n- `allow-select`", + "type": "string", + "const": "default", + "markdownDescription": "### Default Permissions\n\nThis permission set configures what kind of\ndatabase operations are available from the sql plugin.\n\n### Granted Permissions\n\nAll reading related operations are enabled.\nAlso allows to load or close a connection.\n\n\n#### This default permission set includes:\n\n- `allow-close`\n- `allow-load`\n- `allow-select`" + } + ] + } + } +} \ No newline at end of file diff --git a/plugins/sql/rollup.config.js b/plugins/sql/rollup.config.js new file mode 100644 index 0000000..1f349ec --- /dev/null +++ b/plugins/sql/rollup.config.js @@ -0,0 +1,7 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +import { createConfig } from '../../shared/rollup.config.js' + +export default createConfig() diff --git a/plugins/sql/src/commands.rs b/plugins/sql/src/commands.rs new file mode 100644 index 0000000..760d00b --- /dev/null +++ b/plugins/sql/src/commands.rs @@ -0,0 +1,80 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use indexmap::IndexMap; +use serde_json::Value as JsonValue; +use sqlx::migrate::Migrator; +use tauri::{command, AppHandle, Runtime, State}; + +use crate::{DbInstances, DbPool, Error, LastInsertId, Migrations}; + +#[command] +pub(crate) async fn load( + app: AppHandle, + db_instances: State<'_, DbInstances>, + migrations: State<'_, Migrations>, + db: String, +) -> Result { + let pool = DbPool::connect(&db, &app).await?; + + if let Some(migrations) = migrations.0.lock().await.remove(&db) { + let migrator = Migrator::new(migrations).await?; + pool.migrate(&migrator).await?; + } + + db_instances.0.write().await.insert(db.clone(), pool); + + Ok(db) +} + +/// Allows the database connection(s) to be closed; if no database +/// name is passed in then _all_ database connection pools will be +/// shut down. +#[command] +pub(crate) async fn close( + db_instances: State<'_, DbInstances>, + db: Option, +) -> Result { + let instances = db_instances.0.read().await; + + let pools = if let Some(db) = db { + vec![db] + } else { + instances.keys().cloned().collect() + }; + + for pool in pools { + let db = instances.get(&pool).ok_or(Error::DatabaseNotLoaded(pool))?; + db.close().await; + } + + Ok(true) +} + +/// Execute a command against the database +#[command] +pub(crate) async fn execute( + db_instances: State<'_, DbInstances>, + db: String, + query: String, + values: Vec, +) -> Result<(u64, LastInsertId), crate::Error> { + let instances = db_instances.0.read().await; + + let db = instances.get(&db).ok_or(Error::DatabaseNotLoaded(db))?; + db.execute(query, values).await +} + +#[command] +pub(crate) async fn select( + db_instances: State<'_, DbInstances>, + db: String, + query: String, + values: Vec, +) -> Result>, crate::Error> { + let instances = db_instances.0.read().await; + + let db = instances.get(&db).ok_or(Error::DatabaseNotLoaded(db))?; + db.select(query, values).await +} diff --git a/plugins/sql/src/decode/mod.rs b/plugins/sql/src/decode/mod.rs new file mode 100644 index 0000000..0a2d2cd --- /dev/null +++ b/plugins/sql/src/decode/mod.rs @@ -0,0 +1,10 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +#[cfg(feature = "mysql")] +pub(crate) mod mysql; +#[cfg(feature = "postgres")] +pub(crate) mod postgres; +#[cfg(feature = "sqlite")] +pub(crate) mod sqlite; diff --git a/plugins/sql/src/decode/mysql.rs b/plugins/sql/src/decode/mysql.rs new file mode 100644 index 0000000..53fd20c --- /dev/null +++ b/plugins/sql/src/decode/mysql.rs @@ -0,0 +1,101 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use serde_json::Value as JsonValue; +use sqlx::{mysql::MySqlValueRef, TypeInfo, Value, ValueRef}; +use time::{Date, OffsetDateTime, PrimitiveDateTime, Time}; + +use crate::Error; + +pub(crate) fn to_json(v: MySqlValueRef) -> Result { + if v.is_null() { + return Ok(JsonValue::Null); + } + + let res = match v.type_info().name() { + "CHAR" | "VARCHAR" | "TINYTEXT" | "TEXT" | "MEDIUMTEXT" | "LONGTEXT" | "ENUM" => { + if let Ok(v) = ValueRef::to_owned(&v).try_decode() { + JsonValue::String(v) + } else { + JsonValue::Null + } + } + "FLOAT" => { + if let Ok(v) = ValueRef::to_owned(&v).try_decode::() { + JsonValue::from(v) + } else { + JsonValue::Null + } + } + "DOUBLE" => { + if let Ok(v) = ValueRef::to_owned(&v).try_decode::() { + JsonValue::from(v) + } else { + JsonValue::Null + } + } + "TINYINT" | "SMALLINT" | "INT" | "MEDIUMINT" | "BIGINT" => { + if let Ok(v) = ValueRef::to_owned(&v).try_decode::() { + JsonValue::Number(v.into()) + } else { + JsonValue::Null + } + } + "TINYINT UNSIGNED" | "SMALLINT UNSIGNED" | "INT UNSIGNED" | "MEDIUMINT UNSIGNED" + | "BIGINT UNSIGNED" | "YEAR" => { + if let Ok(v) = ValueRef::to_owned(&v).try_decode::() { + JsonValue::Number(v.into()) + } else { + JsonValue::Null + } + } + "BOOLEAN" => { + if let Ok(v) = ValueRef::to_owned(&v).try_decode() { + JsonValue::Bool(v) + } else { + JsonValue::Null + } + } + "DATE" => { + if let Ok(v) = ValueRef::to_owned(&v).try_decode::() { + JsonValue::String(v.to_string()) + } else { + JsonValue::Null + } + } + "TIME" => { + if let Ok(v) = ValueRef::to_owned(&v).try_decode::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 AivR!s literal 0 HcmV?d00001 diff --git a/plugins/store/build.rs b/plugins/store/build.rs new file mode 100644 index 0000000..2e88d59 --- /dev/null +++ b/plugins/store/build.rs @@ -0,0 +1,26 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +const COMMANDS: &[&str] = &[ + "load", + "get_store", + "set", + "get", + "has", + "delete", + "clear", + "reset", + "keys", + "values", + "entries", + "length", + "reload", + "save", +]; + +fn main() { + tauri_plugin::Builder::new(COMMANDS) + .global_api_script_path("./api-iife.js") + .build(); +} diff --git a/plugins/store/examples/AppSettingsManager/.gitignore b/plugins/store/examples/AppSettingsManager/.gitignore new file mode 100644 index 0000000..a9b26d1 --- /dev/null +++ b/plugins/store/examples/AppSettingsManager/.gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +#dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/plugins/store/examples/AppSettingsManager/.vscode/extensions.json b/plugins/store/examples/AppSettingsManager/.vscode/extensions.json new file mode 100644 index 0000000..24d7cc6 --- /dev/null +++ b/plugins/store/examples/AppSettingsManager/.vscode/extensions.json @@ -0,0 +1,3 @@ +{ + "recommendations": ["tauri-apps.tauri-vscode", "rust-lang.rust-analyzer"] +} diff --git a/plugins/store/examples/AppSettingsManager/README.md b/plugins/store/examples/AppSettingsManager/README.md new file mode 100644 index 0000000..b381dcf --- /dev/null +++ b/plugins/store/examples/AppSettingsManager/README.md @@ -0,0 +1,7 @@ +# Tauri + Vanilla TS + +This template should help get you started developing with Tauri in vanilla HTML, CSS and Typescript. + +## Recommended IDE Setup + +- [VS Code](https://code.visualstudio.com/) + [Tauri](https://marketplace.visualstudio.com/items?itemName=tauri-apps.tauri-vscode) + [rust-analyzer](https://marketplace.visualstudio.com/items?itemName=rust-lang.rust-analyzer) diff --git a/plugins/store/examples/AppSettingsManager/index.html b/plugins/store/examples/AppSettingsManager/index.html new file mode 100644 index 0000000..7c26869 --- /dev/null +++ b/plugins/store/examples/AppSettingsManager/index.html @@ -0,0 +1,54 @@ + + + + + + + Tauri App + + + + + + + + diff --git a/plugins/store/examples/AppSettingsManager/package.json b/plugins/store/examples/AppSettingsManager/package.json new file mode 100644 index 0000000..f3907b4 --- /dev/null +++ b/plugins/store/examples/AppSettingsManager/package.json @@ -0,0 +1,15 @@ +{ + "private": true, + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc && vite build", + "preview": "vite preview", + "tauri": "tauri" + }, + "devDependencies": { + "@tauri-apps/cli": "2.11.4", + "typescript": "^6.0.3", + "vite": "^8.0.16" + } +} diff --git a/plugins/store/examples/AppSettingsManager/src-tauri/.gitignore b/plugins/store/examples/AppSettingsManager/src-tauri/.gitignore new file mode 100644 index 0000000..043cc4a --- /dev/null +++ b/plugins/store/examples/AppSettingsManager/src-tauri/.gitignore @@ -0,0 +1,4 @@ +# Generated by Cargo +# will have compiled files and executables +/target/ +gen/schemas diff --git a/plugins/store/examples/AppSettingsManager/src-tauri/Cargo.toml b/plugins/store/examples/AppSettingsManager/src-tauri/Cargo.toml new file mode 100644 index 0000000..d005f10 --- /dev/null +++ b/plugins/store/examples/AppSettingsManager/src-tauri/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "app_settings_manager" +version = "0.0.0" +description = "A Tauri App" +authors = ["you"] +license = "" +repository = "" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[build-dependencies] +tauri-build = { workspace = true } + +[dependencies] +tauri = { workspace = true, features = ["wry", "common-controls-v6", "x11"] } +serde = { workspace = true } +serde_json = { workspace = true } +tauri-plugin-store = { path = "../../../" } diff --git a/plugins/store/examples/AppSettingsManager/src-tauri/build.rs b/plugins/store/examples/AppSettingsManager/src-tauri/build.rs new file mode 100644 index 0000000..5ebf8d2 --- /dev/null +++ b/plugins/store/examples/AppSettingsManager/src-tauri/build.rs @@ -0,0 +1,7 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +fn main() { + tauri_build::build() +} diff --git a/plugins/store/examples/AppSettingsManager/src-tauri/icons/128x128.png b/plugins/store/examples/AppSettingsManager/src-tauri/icons/128x128.png new file mode 100644 index 0000000000000000000000000000000000000000..6be5e50e9b9ae84d9e2ee433f32ef446495eaf3b GIT binary patch literal 3512 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 literal 0 HcmV?d00001 diff --git a/plugins/store/examples/AppSettingsManager/src-tauri/icons/128x128@2x.png b/plugins/store/examples/AppSettingsManager/src-tauri/icons/128x128@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..e81becee571e96f76aa5667f9324c05e5e7a4479 GIT binary patch literal 7012 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 Sh@|Ye~|0>}wMPLT8jm@Y!H33Sz}5aFI6 zM9Lzqz|;A*0sGs=2A1uU!1nk2dGF7knQwr99SAFen)x(eCO;F8y2C~0FD1YxRTPcy zPWVxkUYmeuz}Tv?7&Fe-!UE{)ZW)Mb;H)^#eHDv$`dkZGguJz@^MA!ZNGAUqt{|0H zpZ7Ch9S`q5!>R%}>}62!+(T^evyO+ImSo2wpu)su4^3nw5(%)KD%gbSev^*HZZ&3( z#&c@Z0gH|}Ck)w6fh0&NBJ62ib%R}(3@$VFl*_#l2W$wQ-~4RmZZAt5O*^2Q5}Xr8Hy@c`#pM?kc?hFWxRXr*mUfUCXf4ka5DD~ zat6d85COB05l#(P9*cQZ3EC8fVdS~?&vN#rce(aF9@xp80O2{{FBvU+{X>Hoh;xI` z{$e^Nw1y*VbO8wv`8|-m?NwNaKGTGaF{P^JLB^DbOYWIbn%eT`*!^C1H36=O8Z-M> zkD~88ry`eSo`tEBN4>w7OWZwUzlh{WM1m8R6zepqGcGMaV7vWY9b?K4b6~|HVG)ec wi>I@ws#sZo7or4_*4M>7;p5{nr2pZ?Uu4>Krr0kU)&Kwi07*qoM6N<$f)&@lf&c&j literal 0 HcmV?d00001 diff --git a/plugins/store/examples/AppSettingsManager/src-tauri/icons/Square107x107Logo.png b/plugins/store/examples/AppSettingsManager/src-tauri/icons/Square107x107Logo.png new file mode 100644 index 0000000000000000000000000000000000000000..0ca4f27198838968bd60ed7d371bfa23496b7fe5 GIT binary patch literal 2863 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 Y2S8W#vM)6=T>t<807*qoM6N<$f*y@n<^TWy literal 0 HcmV?d00001 diff --git a/plugins/store/examples/AppSettingsManager/src-tauri/icons/Square284x284Logo.png b/plugins/store/examples/AppSettingsManager/src-tauri/icons/Square284x284Logo.png new file mode 100644 index 0000000000000000000000000000000000000000..c021d2ba76619c08969ab688db3b27f29257aa6f GIT binary patch literal 7737 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 literal 0 HcmV?d00001 diff --git a/plugins/store/examples/AppSettingsManager/src-tauri/icons/Square30x30Logo.png b/plugins/store/examples/AppSettingsManager/src-tauri/icons/Square30x30Logo.png new file mode 100644 index 0000000000000000000000000000000000000000..621970023096ed9f494ba18ace15421a45cd65fa GIT binary patch literal 903 zcmV;219<$2P)2 z+CUKPMqaqGiH;zb!R4$B-WXS^YzQr=@UH>k4?*L)&R=zYjBrZenKdc9|JlS$SO*RJ zKt8FSTDAdk1g_WPAO!p^V!AuL;Lm;uQyV;zKq)J3i(;q*;k+pD%f3eltU`PYdy9(k0&%` zuWAPcV6|-y?|?7O1W!KSK}pbk8#~!|FA@(VJkt^V@0lio{afoAeo*f&$W2s6${5!1eKvAGD2$GZwSB98L2ZVS- zKn8ENRkZ*sb!@QugOrQNK3(sy1v%J#m|rpB+h|Nkqa3FRT>74xSs{#&saU2Lf!_Iq zKmuKAESh`gs!fneGWn+nf}l?7jE$HW!Af&vE5=G!QU)U2v&HLIBGXKk4nQx{hsHjL zLPMAo5=*uInFbq7(aa`Y2VX5wCmaeqvECOFv)a>0t>ZaEb*cJccER=BB?KFZhV$c^ znL*l8x*UYZv4WK|j?~Jt6~~F%{pk~z5A*>^M`?r5m9@RJ_x|uEtX(6Vk@Y()MVto* z93wr)%3m%|#OZ~srm>zF(JvDuTq*@;d&^>_BJm5hOU`3FjG70L#Vzv9I?`<7$T@

jU?lMi@tgxr7CqX_r3uw^y4tVU3Pm0sw;|1WSUO%?=bG`*Kmz6u4{#ti;T7AWIBAEh!(Y zz>O01&#X?Ds@L)Sb{CkG#Yz4$3o d@96)?#cz^xWoA}>B$xmI002ovPDHLkV1l3&k#zt7 literal 0 HcmV?d00001 diff --git a/plugins/store/examples/AppSettingsManager/src-tauri/icons/Square310x310Logo.png b/plugins/store/examples/AppSettingsManager/src-tauri/icons/Square310x310Logo.png new file mode 100644 index 0000000000000000000000000000000000000000..f9bc04839491e66c07b16ab03743c0c53b4109cc GIT binary patch literal 8591 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 JPDHLkV1lm|RXG3v literal 0 HcmV?d00001 diff --git a/plugins/store/examples/AppSettingsManager/src-tauri/icons/Square71x71Logo.png b/plugins/store/examples/AppSettingsManager/src-tauri/icons/Square71x71Logo.png new file mode 100644 index 0000000000000000000000000000000000000000..63440d7984936a9caa89275928d8dce97e4d033b GIT binary patch literal 2011 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∓t$dRJGIadE?vc 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 Kx8Vjl{{H|h@<;Lj literal 0 HcmV?d00001 diff --git a/plugins/store/examples/AppSettingsManager/src-tauri/icons/icon.ico b/plugins/store/examples/AppSettingsManager/src-tauri/icons/icon.ico new file mode 100644 index 0000000000000000000000000000000000000000..b3636e4b22ba65db9061cd60a77b02c92022dfd6 GIT binary patch literal 86642 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 AKmY&$ literal 0 HcmV?d00001 diff --git a/plugins/store/examples/AppSettingsManager/src-tauri/icons/icon.png b/plugins/store/examples/AppSettingsManager/src-tauri/icons/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..e1cd2619e0b5ec089cbba5ec7b03ddf2b1dfceb6 GIT binary patch literal 14183 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

5l#v3A#xI3*z~1~fvo9Q(N^(==!|_FZ z*duZ=+M1~)8E|otX8KNZlr?qels#x_1Xq@9IIw~@9uAREJVH)Xw^}UclF6327}E42 zT)E&?U%TK?(+K7%R!`H5oX0i)4Qn5??Iw3p5J~6_u+aWehY{DSn}3V2p$bgjnAu?o)v@iC254fXeMv50$9YrpU`N?u@QIWs)T?SP|fa}(|9 zqAX+!7`cx=4)cCBg5h~pu(?@9`)aCr#oyz$ld=#RFxYCNZCZls@4v2~*e-t6PEVvV z&bbK3b3wt(Coc!ufAbXXC<**#HQ%J9k`New6iG<5RjtO4XVO?dCvwxD{kJ#tfQr(X zg^NTwF-FwAeS_{V4bfel8l`~NbfrTR2s!G>WduFWxH(t~aK4q=6rEE^$+Uox>gJO2 z{L<;6Q6nHa5#ZEM>H58not!)z(6*_=^~8}jWf*IG$AUKVWOZ4?)GfF z+BM#*wKKmLFD7E~W3U!$IVm$k_k1f&Kz6WV8@55P?r~bcg-Za-!rvW?ns&)KOGT2~ zlkAyqhQj=P$Eg3w#K~}zH@J5bo-BfHjInKSz$@?+Z)NPD4pHj^_Qxmi`UqoTy=`sV zLVxrXGuBr=QRm|}wg75yetQQK4fY3#P_~J}zEfPnb2C4Wo!E(d*(cA;b?7$g2in<( zPn)ghX}nzJPmb6(3Dpeg_GW~Hc}Lt=lgsSZz z!5QXyz7KaR;D`3Ee}d`af{H>WWZ|Io1QI3~4Ll_`g1(cRnhLK73Ro)7zPCd={1W2x zRp%Xlvv4>!<2@}$hz|!V{T}_eHx2xkLl^hQoZTCnsjCl|W_@5Fx2(+j0ogy&Y+;L- z<)G$*CiN7hOm^s!{U>1F7U=iNk{+u~dAC!eDz%=|glFW0jEZU1&o(G_c#wTxUjnG} z#cg3>jEpUi#Mlq@t?Msg_#geK^Lx@DyHWf7=AS5vVyM7YOjvUVCfcpVR<(+5!H?9- zySI6s>o3m&*zr||=wcPGyBkQV`EWJl@bH8qobjOp+sXL*)=&yX)8aAbf~tGv?a2SN zu^Ddo-z?DWk9h9Yz#5p^NU#x~wYSd?H@w@!2Gb4G)6-utEMV~~M85Br5ff(v5O1|T z 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 From<&Store> for AppSettings { + fn from(store: &Store) -> Self { + let launch_at_login = store + .get("appSettings.launchAtLogin") + .and_then(|v| v.as_bool()) + .unwrap_or(false); + + let theme = store + .get("appSettings.theme") + .and_then(|v| v.as_str().map(String::from)) + .unwrap_or_else(|| "dark".to_owned()); + + AppSettings { + launch_at_login, + theme, + } + } +} diff --git a/plugins/store/examples/AppSettingsManager/src-tauri/src/main.rs b/plugins/store/examples/AppSettingsManager/src-tauri/src/main.rs new file mode 100644 index 0000000..a23585b --- /dev/null +++ b/plugins/store/examples/AppSettingsManager/src-tauri/src/main.rs @@ -0,0 +1,41 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +// Prevents additional console window on Windows in release, DO NOT REMOVE!! +#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] + +use serde_json::json; +use tauri::Listener; +use tauri_plugin_store::StoreExt; + +mod app; +use app::settings::AppSettings; + +fn main() { + tauri::Builder::default() + .plugin(tauri_plugin_store::Builder::new().build()) + .setup(|app| { + // Init store and load it from disk + let store = app.store("settings.json")?; + + app.listen("store://change", |event| { + dbg!(event); + }); + + let app_settings = AppSettings::from(store.as_ref()); + let theme = app_settings.theme; + let launch_at_login = app_settings.launch_at_login; + + println!("theme {theme}"); + println!("launch_at_login {launch_at_login}"); + store.set( + "appSettings", + json!({ "theme": theme, "launchAtLogin": launch_at_login }), + ); + + Ok(()) + }) + .run(tauri::generate_context!()) + .expect("error while running tauri application"); +} diff --git a/plugins/store/examples/AppSettingsManager/src-tauri/tauri.conf.json b/plugins/store/examples/AppSettingsManager/src-tauri/tauri.conf.json new file mode 100644 index 0000000..d3f60da --- /dev/null +++ b/plugins/store/examples/AppSettingsManager/src-tauri/tauri.conf.json @@ -0,0 +1,34 @@ +{ + "productName": "app", + "version": "0.1.0", + "identifier": "com.tauri.app-settings-manager", + "build": { + "devUrl": "http://localhost:1420", + "frontendDist": "../dist" + }, + "app": { + "windows": [ + { + "title": "app", + "width": 800, + "height": 600, + "resizable": true, + "fullscreen": false + } + ], + "security": { + "csp": "default-src blob: data: filesystem: ws: wss: http: https: tauri: http://tauri.localhost 'unsafe-eval' 'unsafe-inline' 'self' img-src: 'self'" + } + }, + "bundle": { + "active": true, + "targets": "all", + "icon": [ + "icons/32x32.png", + "icons/128x128.png", + "icons/128x128@2x.png", + "icons/icon.icns", + "icons/icon.ico" + ] + } +} diff --git a/plugins/store/examples/AppSettingsManager/src/assets/tauri.svg b/plugins/store/examples/AppSettingsManager/src/assets/tauri.svg new file mode 100644 index 0000000..31b62c9 --- /dev/null +++ b/plugins/store/examples/AppSettingsManager/src/assets/tauri.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/plugins/store/examples/AppSettingsManager/src/assets/typescript.svg b/plugins/store/examples/AppSettingsManager/src/assets/typescript.svg new file mode 100644 index 0000000..30a5edd --- /dev/null +++ b/plugins/store/examples/AppSettingsManager/src/assets/typescript.svg @@ -0,0 +1,25 @@ + + + + + + + + diff --git a/plugins/store/examples/AppSettingsManager/src/assets/vite.svg b/plugins/store/examples/AppSettingsManager/src/assets/vite.svg new file mode 100644 index 0000000..e7b8dfb --- /dev/null +++ b/plugins/store/examples/AppSettingsManager/src/assets/vite.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/plugins/store/examples/AppSettingsManager/src/main.ts b/plugins/store/examples/AppSettingsManager/src/main.ts new file mode 100644 index 0000000..57cbe4b --- /dev/null +++ b/plugins/store/examples/AppSettingsManager/src/main.ts @@ -0,0 +1,9 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +window.addEventListener('DOMContentLoaded', () => { + document.querySelector('#greet-form')?.addEventListener('submit', (e) => { + e.preventDefault() + }) +}) diff --git a/plugins/store/examples/AppSettingsManager/src/styles.css b/plugins/store/examples/AppSettingsManager/src/styles.css new file mode 100644 index 0000000..f7de85b --- /dev/null +++ b/plugins/store/examples/AppSettingsManager/src/styles.css @@ -0,0 +1,109 @@ +:root { + font-family: Inter, Avenir, Helvetica, Arial, sans-serif; + font-size: 16px; + line-height: 24px; + font-weight: 400; + + color: #0f0f0f; + background-color: #f6f6f6; + + font-synthesis: none; + text-rendering: optimizeLegibility; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + -webkit-text-size-adjust: 100%; +} + +.container { + margin: 0; + padding-top: 10vh; + display: flex; + flex-direction: column; + justify-content: center; + text-align: center; +} + +.logo { + height: 6em; + padding: 1.5em; + will-change: filter; + transition: 0.75s; +} + +.logo.tauri:hover { + filter: drop-shadow(0 0 2em #24c8db); +} + +.row { + display: flex; + justify-content: center; +} + +a { + font-weight: 500; + color: #646cff; + text-decoration: inherit; +} + +a:hover { + color: #535bf2; +} + +h1 { + text-align: center; +} + +input, +button { + border-radius: 8px; + border: 1px solid transparent; + padding: 0.6em 1.2em; + font-size: 1em; + font-weight: 500; + font-family: inherit; + color: #0f0f0f; + background-color: #ffffff; + transition: border-color 0.25s; + box-shadow: 0 2px 2px rgba(0, 0, 0, 0.2); +} + +button { + cursor: pointer; +} + +button:hover { + border-color: #396cd8; +} +button:active { + border-color: #396cd8; + background-color: #e8e8e8; +} + +input, +button { + outline: none; +} + +#greet-input { + margin-right: 5px; +} + +@media (prefers-color-scheme: dark) { + :root { + color: #f6f6f6; + background-color: #2f2f2f; + } + + a:hover { + color: #24c8db; + } + + input, + button { + color: #ffffff; + background-color: #0f0f0f98; + } + button:active { + background-color: #0f0f0f69; + } +} diff --git a/plugins/store/examples/AppSettingsManager/tsconfig.json b/plugins/store/examples/AppSettingsManager/tsconfig.json new file mode 100644 index 0000000..75abdef --- /dev/null +++ b/plugins/store/examples/AppSettingsManager/tsconfig.json @@ -0,0 +1,23 @@ +{ + "compilerOptions": { + "target": "ES2020", + "useDefineForClassFields": true, + "module": "ESNext", + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["src"] +} diff --git a/plugins/store/examples/AppSettingsManager/vite.config.ts b/plugins/store/examples/AppSettingsManager/vite.config.ts new file mode 100644 index 0000000..661eb23 --- /dev/null +++ b/plugins/store/examples/AppSettingsManager/vite.config.ts @@ -0,0 +1,21 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +import { defineConfig } from 'vite' + +// https://vitejs.dev/config/ +export default defineConfig(async () => ({ + // Vite options tailored for Tauri development and only applied in `tauri dev` or `tauri build` + // + // 1. prevent vite from obscuring rust errors + clearScreen: false, + // 2. tauri expects a fixed port, fail if that port is not available + server: { + port: 1420, + strictPort: true + }, + // 3. to make use of `TAURI_DEBUG` and other env variables + // https://tauri.studio/v1/api/config#buildconfig.beforedevcommand + envPrefix: ['VITE_', 'TAURI_'] +})) diff --git a/plugins/store/guest-js/index.ts b/plugins/store/guest-js/index.ts new file mode 100644 index 0000000..fdec3e8 --- /dev/null +++ b/plugins/store/guest-js/index.ts @@ -0,0 +1,933 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +/** + * Simple, persistent key-value store. + * + * A store is persisted to a file inside the application data directory and is shared with the + * Rust side of the application, which can read and write the same store through its own API. + * + * @module + */ + +import { listen, type UnlistenFn } from '@tauri-apps/api/event' + +import { invoke, Resource } from '@tauri-apps/api/core' + +interface ChangePayload { + path: string + resourceId?: number + key: string + value: T + exists: boolean +} + +/** + * Options to create a store + */ +export type StoreOptions = { + /** + * Default value of the store + */ + defaults?: { [key: string]: unknown } + /** + * Auto save on modification with debounce duration in milliseconds, it's 100ms by default, pass in `false` to disable it + */ + autoSave?: boolean | number + /** + * Name of a serialize function registered in the rust side plugin builder + */ + serializeFnName?: string + /** + * Name of a deserialize function registered in the rust side plugin builder + */ + deserializeFnName?: string + /** + * Force create a new store with default values even if it already exists. + */ + createNew?: boolean + /** + * When creating the store, override the store with the on-disk state if it exists, ignoring defaults + */ + overrideDefaults?: boolean +} + +/** + * Create a new Store or load the existing store with the path. + * + * If the file at the given path does not exist yet, the store is created in memory with the + * configured defaults and the file is only written on the first save. + * + * @example + * ```typescript + * import { load } from '@tauri-apps/plugin-store'; + * const store = await load('store.json'); + * ``` + * + * @param path Path to save the store in `app_data_dir` + * @param options Store configuration options + * @returns A promise resolving to the loaded store. + * + * @since 2.1.0 + */ +export async function load( + path: string, + options?: StoreOptions +): Promise { + return await Store.load(path, options) +} + +/** + * Gets an already loaded store. + * + * If the store is not loaded, returns `null`. In this case you must {@link Store.load load} it. + * + * This function is more useful when you already know the store is loaded + * and just need to access its instance. Prefer {@link Store.load} otherwise. + * + * @example + * ```typescript + * import { getStore } from '@tauri-apps/plugin-store'; + * const store = await getStore('store.json'); + * ``` + * + * @param path Path of the store. + * @returns A promise resolving to the store instance, or `null` if it is not loaded. + * + * @since 2.1.0 + */ +export async function getStore(path: string): Promise { + return await Store.get(path) +} + +/** + * A lazy loaded key-value store persisted by the backend layer. + * + * The underlying {@linkcode Store} is only created or loaded when one of the methods of this + * class is called for the first time, and every call afterwards reuses that same instance. + * + * @since 2.1.0 + */ +export class LazyStore implements IStore { + private _store?: Promise + + private get store(): Promise { + if (!this._store) { + this._store = load(this.path, this.options) + } + return this._store + } + + /** + * Creates a handle to the store at the given path without loading it yet. + * + * Note that the options are not applied if someone else already created the store + * + * @param path Path to save the store in `app_data_dir` + * @param options Store configuration options + * + * @example + * ```typescript + * import { LazyStore } from '@tauri-apps/plugin-store'; + * const store = new LazyStore('store.json'); + * ``` + */ + constructor( + private readonly path: string, + private readonly options?: StoreOptions + ) {} + + /** + * Init/load the store if it's not loaded already + * + * @example + * ```typescript + * import { LazyStore } from '@tauri-apps/plugin-store'; + * const store = new LazyStore('store.json'); + * await store.init(); + * ``` + */ + async init(): Promise { + await this.store + } + + /** + * Inserts a key-value pair into the store, loading it first if needed. + * + * Delegates to {@linkcode Store.set} on the underlying store. + * + * @example + * ```typescript + * import { LazyStore } from '@tauri-apps/plugin-store'; + * const store = new LazyStore('store.json'); + * await store.set('some-key', { value: 5 }); + * ``` + * + * @param key The key to insert the value at. + * @param value The value to store, which must be serializable to JSON. + */ + async set(key: string, value: unknown): Promise { + return (await this.store).set(key, value) + } + + /** + * Returns the value for the given `key` or `undefined` if the key does not exist. + * + * Delegates to {@linkcode Store.get} on the underlying store, loading it first if needed. + * + * @example + * ```typescript + * import { LazyStore } from '@tauri-apps/plugin-store'; + * const store = new LazyStore('store.json'); + * const value = await store.get<{ value: number }>('some-key'); + * ``` + * + * @param key The key to read the value of. + * @returns A promise resolving to the stored value, or `undefined` if the key does not exist. + */ + async get(key: string): Promise { + return (await this.store).get(key) + } + + /** + * Returns `true` if the given `key` exists in the store. + * + * Delegates to {@linkcode Store.has} on the underlying store, loading it first if needed. + * + * @example + * ```typescript + * import { LazyStore } from '@tauri-apps/plugin-store'; + * const store = new LazyStore('store.json'); + * const exists = await store.has('some-key'); + * ``` + * + * @param key The key to check. + * @returns A promise resolving to `true` if the key exists in the store. + */ + async has(key: string): Promise { + return (await this.store).has(key) + } + + /** + * Removes a key-value pair from the store. + * + * Delegates to {@linkcode Store.delete} on the underlying store, loading it first if needed. + * + * @example + * ```typescript + * import { LazyStore } from '@tauri-apps/plugin-store'; + * const store = new LazyStore('store.json'); + * const removed = await store.delete('some-key'); + * ``` + * + * @param key The key to remove. + * @returns A promise resolving to `true` if the key existed and was removed. + */ + async delete(key: string): Promise { + return (await this.store).delete(key) + } + + /** + * Clears the store, removing all key-value pairs. + * + * Note: To clear the storage and reset it to its `default` value, use {@linkcode reset} instead. + * Delegates to {@linkcode Store.clear} on the underlying store, loading it first if needed. + * + * @example + * ```typescript + * import { LazyStore } from '@tauri-apps/plugin-store'; + * const store = new LazyStore('store.json'); + * await store.clear(); + * ``` + */ + async clear(): Promise { + await (await this.store).clear() + } + + /** + * Resets the store to its `default` value. + * + * If no default value has been set, this method behaves identical to {@linkcode clear}. + * Delegates to {@linkcode Store.reset} on the underlying store, loading it first if needed. + * + * @example + * ```typescript + * import { LazyStore } from '@tauri-apps/plugin-store'; + * const store = new LazyStore('store.json', { defaults: { 'some-key': 0 } }); + * await store.reset(); + * ``` + */ + async reset(): Promise { + await (await this.store).reset() + } + + /** + * Returns a list of all keys in the store. + * + * Delegates to {@linkcode Store.keys} on the underlying store, loading it first if needed. + * + * @example + * ```typescript + * import { LazyStore } from '@tauri-apps/plugin-store'; + * const store = new LazyStore('store.json'); + * const keys = await store.keys(); + * ``` + * + * @returns A promise resolving to the list of keys, in arbitrary order. + */ + async keys(): Promise { + return (await this.store).keys() + } + + /** + * Returns a list of all values in the store. + * + * Delegates to {@linkcode Store.values} on the underlying store, loading it first if needed. + * + * @example + * ```typescript + * import { LazyStore } from '@tauri-apps/plugin-store'; + * const store = new LazyStore('store.json'); + * const values = await store.values(); + * ``` + * + * @returns A promise resolving to the list of values, in arbitrary order. + */ + async values(): Promise { + return (await this.store).values() + } + + /** + * Returns a list of all entries in the store. + * + * Delegates to {@linkcode Store.entries} on the underlying store, loading it first if needed. + * + * @example + * ```typescript + * import { LazyStore } from '@tauri-apps/plugin-store'; + * const store = new LazyStore('store.json'); + * const entries = await store.entries(); + * ``` + * + * @returns A promise resolving to the list of key-value pairs, in arbitrary order. + */ + async entries(): Promise> { + return (await this.store).entries() + } + + /** + * Returns the number of key-value pairs in the store. + * + * Delegates to {@linkcode Store.length} on the underlying store, loading it first if needed. + * + * @example + * ```typescript + * import { LazyStore } from '@tauri-apps/plugin-store'; + * const store = new LazyStore('store.json'); + * const length = await store.length(); + * ``` + * + * @returns A promise resolving to the number of key-value pairs in the store. + */ + async length(): Promise { + return (await this.store).length() + } + + /** + * Attempts to load the on-disk state at the store's `path` into memory. + * + * Delegates to {@linkcode Store.reload} on the underlying store, loading it first if needed. + * + * @example + * ```typescript + * import { LazyStore } from '@tauri-apps/plugin-store'; + * const store = new LazyStore('store.json'); + * await store.reload({ ignoreDefaults: true }); + * ``` + * + * @param options Options to change how the on-disk state is merged into the store. + */ + async reload(options?: ReloadOptions): Promise { + await (await this.store).reload(options) + } + + /** + * Saves the store to disk at the store's `path`. + * + * Delegates to {@linkcode Store.save} on the underlying store, loading it first if needed. + * + * @example + * ```typescript + * import { LazyStore } from '@tauri-apps/plugin-store'; + * const store = new LazyStore('store.json'); + * await store.save(); + * ``` + */ + async save(): Promise { + await (await this.store).save() + } + + /** + * Listen to changes on a store key. + * + * Delegates to {@linkcode Store.onKeyChange} on the underlying store, loading it first if needed. + * + * @example + * ```typescript + * import { LazyStore } from '@tauri-apps/plugin-store'; + * const store = new LazyStore('store.json'); + * const unlisten = await store.onKeyChange<{ value: number }>('some-key', (value) => { + * console.log(value); + * }); + * ``` + * + * @param key The key to watch for changes. + * @param cb Callback invoked with the new value, or `undefined` when the key was removed. + * @returns A promise resolving to a function to unlisten to the event. + */ + async onKeyChange( + key: string, + cb: (value: T | undefined) => void + ): Promise { + return (await this.store).onKeyChange(key, cb) + } + + /** + * Listen to changes on the store. + * + * Delegates to {@linkcode Store.onChange} on the underlying store, loading it first if needed. + * + * @example + * ```typescript + * import { LazyStore } from '@tauri-apps/plugin-store'; + * const store = new LazyStore('store.json'); + * const unlisten = await store.onChange<{ value: number }>((key, value) => { + * console.log(key, value); + * }); + * ``` + * + * @param cb Callback invoked with the changed key and its new value, which is `undefined` when the key was removed. + * @returns A promise resolving to a function to unlisten to the event. + */ + async onChange( + cb: (key: string, value: T | undefined) => void + ): Promise { + return (await this.store).onChange(cb) + } + + /** + * Close the store and cleans up this resource from memory. + * **You should not call any method on this object anymore and should drop any reference to it.** + * + * Delegates to {@linkcode Store.close} on the underlying store. + * If the store was never loaded, this method does nothing. + * + * @example + * ```typescript + * import { LazyStore } from '@tauri-apps/plugin-store'; + * const store = new LazyStore('store.json'); + * await store.close(); + * ``` + */ + async close(): Promise { + if (this._store) { + await (await this._store).close() + } + } +} + +/** + * A key-value store persisted by the backend layer. + * + * The values are kept in memory and written to the store's file on {@linkcode Store.save}, + * and automatically after every modification unless auto save is disabled with + * {@linkcode StoreOptions.autoSave}. + * + * @since 2.0.0 + */ +export class Store extends Resource implements IStore { + private constructor(rid: number) { + super(rid) + } + + /** + * Create a new Store or load the existing store with the path. + * + * If the file at the given path does not exist yet, the store is created in memory with the + * configured defaults and the file is only written on the first save. + * + * @example + * ```typescript + * import { Store } from '@tauri-apps/plugin-store'; + * const store = await Store.load('store.json'); + * ``` + * + * @param path Path to save the store in `app_data_dir` + * @param options Store configuration options + * @returns A promise resolving to the loaded store. + */ + static async load(path: string, options?: StoreOptions): Promise { + const rid = await invoke('plugin:store|load', { + path, + options + }) + return new Store(rid) + } + + /** + * Gets an already loaded store. + * + * If the store is not loaded, returns `null`. In this case you must {@link Store.load load} it. + * + * This function is more useful when you already know the store is loaded + * and just need to access its instance. Prefer {@link Store.load} otherwise. + * + * @example + * ```typescript + * import { Store } from '@tauri-apps/plugin-store'; + * let store = await Store.get('store.json'); + * if (!store) { + * store = await Store.load('store.json'); + * } + * ``` + * + * @param path Path of the store. + * @returns A promise resolving to the store instance, or `null` if it is not loaded. + */ + static async get(path: string): Promise { + return await invoke('plugin:store|get_store', { path }).then( + (rid) => (rid ? new Store(rid) : null) + ) + } + + /** + * Inserts a key-value pair into the store. + * + * A change event is emitted for the key and, unless auto save is disabled, the store is + * scheduled to be written to disk. + * + * @example + * ```typescript + * import { Store } from '@tauri-apps/plugin-store'; + * const store = await Store.load('store.json'); + * await store.set('some-key', { value: 5 }); + * ``` + * + * @param key The key to insert the value at. + * @param value The value to store, which must be serializable to JSON. + */ + async set(key: string, value: unknown): Promise { + await invoke('plugin:store|set', { + rid: this.rid, + key, + value + }) + } + + /** + * Returns the value for the given `key` or `undefined` if the key does not exist. + * + * @example + * ```typescript + * import { Store } from '@tauri-apps/plugin-store'; + * const store = await Store.load('store.json'); + * const value = await store.get<{ value: number }>('some-key'); + * ``` + * + * @param key The key to read the value of. + * @returns A promise resolving to the stored value, or `undefined` if the key does not exist. + */ + async get(key: string): Promise { + const [value, exists] = await invoke<[T, boolean]>('plugin:store|get', { + rid: this.rid, + key + }) + return exists ? value : undefined + } + + /** + * Returns `true` if the given `key` exists in the store. + * + * @example + * ```typescript + * import { Store } from '@tauri-apps/plugin-store'; + * const store = await Store.load('store.json'); + * const exists = await store.has('some-key'); + * ``` + * + * @param key The key to check. + * @returns A promise resolving to `true` if the key exists in the store. + */ + async has(key: string): Promise { + return await invoke('plugin:store|has', { + rid: this.rid, + key + }) + } + + /** + * Removes a key-value pair from the store. + * + * A change event is emitted when the key existed and, unless auto save is disabled, the store + * is scheduled to be written to disk. + * + * @example + * ```typescript + * import { Store } from '@tauri-apps/plugin-store'; + * const store = await Store.load('store.json'); + * const removed = await store.delete('some-key'); + * ``` + * + * @param key The key to remove. + * @returns A promise resolving to `true` if the key existed and was removed. + */ + async delete(key: string): Promise { + return await invoke('plugin:store|delete', { + rid: this.rid, + key + }) + } + + /** + * Clears the store, removing all key-value pairs. + * + * Note: To clear the storage and reset it to its `default` value, use {@linkcode reset} instead. + * A change event is emitted for every removed key. + * + * @example + * ```typescript + * import { Store } from '@tauri-apps/plugin-store'; + * const store = await Store.load('store.json'); + * await store.clear(); + * ``` + */ + async clear(): Promise { + await invoke('plugin:store|clear', { rid: this.rid }) + } + + /** + * Resets the store to its `default` value. + * + * If no default value has been set, this method behaves identical to {@linkcode clear}. + * A change event is emitted for every key whose value changed. + * + * @example + * ```typescript + * import { Store } from '@tauri-apps/plugin-store'; + * const store = await Store.load('store.json', { defaults: { 'some-key': 0 } }); + * await store.reset(); + * ``` + */ + async reset(): Promise { + await invoke('plugin:store|reset', { rid: this.rid }) + } + + /** + * Returns a list of all keys in the store. + * + * @example + * ```typescript + * import { Store } from '@tauri-apps/plugin-store'; + * const store = await Store.load('store.json'); + * const keys = await store.keys(); + * ``` + * + * @returns A promise resolving to the list of keys, in arbitrary order. + */ + async keys(): Promise { + return await invoke('plugin:store|keys', { rid: this.rid }) + } + + /** + * Returns a list of all values in the store. + * + * @example + * ```typescript + * import { Store } from '@tauri-apps/plugin-store'; + * const store = await Store.load('store.json'); + * const values = await store.values(); + * ``` + * + * @returns A promise resolving to the list of values, in arbitrary order. + */ + async values(): Promise { + return await invoke('plugin:store|values', { rid: this.rid }) + } + + /** + * Returns a list of all entries in the store. + * + * @example + * ```typescript + * import { Store } from '@tauri-apps/plugin-store'; + * const store = await Store.load('store.json'); + * const entries = await store.entries(); + * ``` + * + * @returns A promise resolving to the list of key-value pairs, in arbitrary order. + */ + async entries(): Promise> { + return await invoke('plugin:store|entries', { rid: this.rid }) + } + + /** + * Returns the number of key-value pairs in the store. + * + * @example + * ```typescript + * import { Store } from '@tauri-apps/plugin-store'; + * const store = await Store.load('store.json'); + * const length = await store.length(); + * ``` + * + * @returns A promise resolving to the number of key-value pairs in the store. + */ + async length(): Promise { + return await invoke('plugin:store|length', { rid: this.rid }) + } + + /** + * Attempts to load the on-disk state at the store's `path` into memory. + * + * This method is useful if the on-disk state was edited by the user and you want to synchronize the changes. + * + * Note: + * - This method loads the data and merges it with the current store, + * this behavior will be changed to resetting to default first and then merging with the on-disk state in v3, + * to fully match the store with the on-disk state, set {@linkcode ReloadOptions | ignoreDefaults} to `true` + * - This method does not emit change events. + * + * @example + * ```typescript + * import { Store } from '@tauri-apps/plugin-store'; + * const store = await Store.load('store.json'); + * await store.reload({ ignoreDefaults: true }); + * ``` + * + * @param options Options to change how the on-disk state is merged into the store. + */ + async reload(options?: ReloadOptions): Promise { + await invoke('plugin:store|reload', { rid: this.rid, ...options }) + } + + /** + * Saves the store to disk at the store's `path`. + * + * Any pending auto save is cancelled, so the store is written exactly once by this call. + * + * @example + * ```typescript + * import { Store } from '@tauri-apps/plugin-store'; + * const store = await Store.load('store.json', { autoSave: false }); + * await store.set('some-key', { value: 5 }); + * await store.save(); + * ``` + */ + async save(): Promise { + await invoke('plugin:store|save', { rid: this.rid }) + } + + /** + * Listen to changes on a store key. + * + * The callback is only invoked for changes made to this store instance. + * + * @example + * ```typescript + * import { Store } from '@tauri-apps/plugin-store'; + * const store = await Store.load('store.json'); + * const unlisten = await store.onKeyChange<{ value: number }>('some-key', (value) => { + * console.log(value); + * }); + * ``` + * + * @param key The key to watch for changes. + * @param cb Callback invoked with the new value, or `undefined` when the key was removed. + * @returns A promise resolving to a function to unlisten to the event. + * + * @since 2.0.0 + */ + async onKeyChange( + key: string, + cb: (value: T | undefined) => void + ): Promise { + return await listen>('store://change', (event) => { + if (event.payload.resourceId === this.rid && event.payload.key === key) { + cb(event.payload.exists ? event.payload.value : undefined) + } + }) + } + + /** + * Listen to changes on the store. + * + * The callback is only invoked for changes made to this store instance. + * + * @example + * ```typescript + * import { Store } from '@tauri-apps/plugin-store'; + * const store = await Store.load('store.json'); + * const unlisten = await store.onChange<{ value: number }>((key, value) => { + * console.log(key, value); + * }); + * ``` + * + * @param cb Callback invoked with the changed key and its new value, which is `undefined` when the key was removed. + * @returns A promise resolving to a function to unlisten to the event. + * + * @since 2.0.0 + */ + async onChange( + cb: (key: string, value: T | undefined) => void + ): Promise { + return await listen>('store://change', (event) => { + if (event.payload.resourceId === this.rid) { + cb( + event.payload.key, + event.payload.exists ? event.payload.value : undefined + ) + } + }) + } +} + +interface IStore { + /** + * Inserts a key-value pair into the store. + * + * @param key + * @param value + * @returns + */ + set(key: string, value: unknown): Promise + + /** + * Returns the value for the given `key` or `undefined` if the key does not exist. + * + * @param key + * @returns + */ + get(key: string): Promise + + /** + * Returns `true` if the given `key` exists in the store. + * + * @param key + * @returns + */ + has(key: string): Promise + + /** + * Removes a key-value pair from the store. + * + * @param key + * @returns + */ + delete(key: string): Promise + + /** + * Clears the store, removing all key-value pairs. + * + * Note: To clear the storage and reset it to its `default` value, use {@linkcode reset} instead. + * @returns + */ + clear(): Promise + + /** + * Resets the store to its `default` value. + * + * If no default value has been set, this method behaves identical to {@linkcode clear}. + * @returns + */ + reset(): Promise + + /** + * Returns a list of all keys in the store. + * + * @returns + */ + keys(): Promise + + /** + * Returns a list of all values in the store. + * + * @returns + */ + values(): Promise + + /** + * Returns a list of all entries in the store. + * + * @returns + */ + entries(): Promise> + + /** + * Returns the number of key-value pairs in the store. + * + * @returns + */ + length(): Promise + + /** + * Attempts to load the on-disk state at the store's `path` into memory. + * + * This method is useful if the on-disk state was edited by the user and you want to synchronize the changes. + * + * Note: + * - This method loads the data and merges it with the current store, + * this behavior will be changed to resetting to default first and then merging with the on-disk state in v3, + * to fully match the store with the on-disk state, set {@linkcode ReloadOptions | ignoreDefaults} to `true` + * - This method does not emit change events. + * + * @returns + */ + reload(options?: ReloadOptions): Promise + + /** + * Saves the store to disk at the store's `path`. + * @returns + */ + save(): Promise + + /** + * Listen to changes on a store key. + * @param key + * @param cb + * @returns A promise resolving to a function to unlisten to the event. + * + * @since 2.0.0 + */ + onKeyChange( + key: string, + cb: (value: T | undefined) => void + ): Promise + + /** + * Listen to changes on the store. + * @param cb + * @returns A promise resolving to a function to unlisten to the event. + * + * @since 2.0.0 + */ + onChange( + cb: (key: string, value: T | undefined) => void + ): Promise + + /** + * Close the store and cleans up this resource from memory. + * **You should not call any method on this object anymore and should drop any reference to it.** + */ + close(): Promise +} + +/** + * Options to change how a store is reloaded from its on-disk state. + */ +export type ReloadOptions = { + /** + * To fully match the store with the on-disk state, ignoring defaults + */ + ignoreDefaults?: boolean +} diff --git a/plugins/store/package.json b/plugins/store/package.json new file mode 100644 index 0000000..79f7924 --- /dev/null +++ b/plugins/store/package.json @@ -0,0 +1,30 @@ +{ + "name": "@tauri-apps/plugin-store", + "version": "2.4.5", + "description": "Simple, persistent key-value store.", + "license": "MIT OR Apache-2.0", + "authors": [ + "Tauri Programme within The Commons Conservancy" + ], + "repository": "https://github.com/tauri-apps/plugins-workspace", + "type": "module", + "types": "./dist-js/index.d.ts", + "main": "./dist-js/index.cjs", + "module": "./dist-js/index.js", + "exports": { + "types": "./dist-js/index.d.ts", + "import": "./dist-js/index.js", + "require": "./dist-js/index.cjs" + }, + "scripts": { + "build": "rollup -c" + }, + "files": [ + "dist-js", + "README.md", + "LICENSE" + ], + "dependencies": { + "@tauri-apps/api": "^2.11.0" + } +} diff --git a/plugins/store/permissions/autogenerated/commands/clear.toml b/plugins/store/permissions/autogenerated/commands/clear.toml new file mode 100644 index 0000000..83de181 --- /dev/null +++ b/plugins/store/permissions/autogenerated/commands/clear.toml @@ -0,0 +1,13 @@ +# Automatically generated - DO NOT EDIT! + +"$schema" = "../../schemas/schema.json" + +[[permission]] +identifier = "allow-clear" +description = "Enables the clear command without any pre-configured scope." +commands.allow = ["clear"] + +[[permission]] +identifier = "deny-clear" +description = "Denies the clear command without any pre-configured scope." +commands.deny = ["clear"] diff --git a/plugins/store/permissions/autogenerated/commands/delete.toml b/plugins/store/permissions/autogenerated/commands/delete.toml new file mode 100644 index 0000000..3d9d523 --- /dev/null +++ b/plugins/store/permissions/autogenerated/commands/delete.toml @@ -0,0 +1,13 @@ +# Automatically generated - DO NOT EDIT! + +"$schema" = "../../schemas/schema.json" + +[[permission]] +identifier = "allow-delete" +description = "Enables the delete command without any pre-configured scope." +commands.allow = ["delete"] + +[[permission]] +identifier = "deny-delete" +description = "Denies the delete command without any pre-configured scope." +commands.deny = ["delete"] diff --git a/plugins/store/permissions/autogenerated/commands/entries.toml b/plugins/store/permissions/autogenerated/commands/entries.toml new file mode 100644 index 0000000..ce8b5f4 --- /dev/null +++ b/plugins/store/permissions/autogenerated/commands/entries.toml @@ -0,0 +1,13 @@ +# Automatically generated - DO NOT EDIT! + +"$schema" = "../../schemas/schema.json" + +[[permission]] +identifier = "allow-entries" +description = "Enables the entries command without any pre-configured scope." +commands.allow = ["entries"] + +[[permission]] +identifier = "deny-entries" +description = "Denies the entries command without any pre-configured scope." +commands.deny = ["entries"] diff --git a/plugins/store/permissions/autogenerated/commands/get.toml b/plugins/store/permissions/autogenerated/commands/get.toml new file mode 100644 index 0000000..d75b403 --- /dev/null +++ b/plugins/store/permissions/autogenerated/commands/get.toml @@ -0,0 +1,13 @@ +# Automatically generated - DO NOT EDIT! + +"$schema" = "../../schemas/schema.json" + +[[permission]] +identifier = "allow-get" +description = "Enables the get command without any pre-configured scope." +commands.allow = ["get"] + +[[permission]] +identifier = "deny-get" +description = "Denies the get command without any pre-configured scope." +commands.deny = ["get"] diff --git a/plugins/store/permissions/autogenerated/commands/get_store.toml b/plugins/store/permissions/autogenerated/commands/get_store.toml new file mode 100644 index 0000000..7c19173 --- /dev/null +++ b/plugins/store/permissions/autogenerated/commands/get_store.toml @@ -0,0 +1,13 @@ +# Automatically generated - DO NOT EDIT! + +"$schema" = "../../schemas/schema.json" + +[[permission]] +identifier = "allow-get-store" +description = "Enables the get_store command without any pre-configured scope." +commands.allow = ["get_store"] + +[[permission]] +identifier = "deny-get-store" +description = "Denies the get_store command without any pre-configured scope." +commands.deny = ["get_store"] diff --git a/plugins/store/permissions/autogenerated/commands/has.toml b/plugins/store/permissions/autogenerated/commands/has.toml new file mode 100644 index 0000000..41d0a93 --- /dev/null +++ b/plugins/store/permissions/autogenerated/commands/has.toml @@ -0,0 +1,13 @@ +# Automatically generated - DO NOT EDIT! + +"$schema" = "../../schemas/schema.json" + +[[permission]] +identifier = "allow-has" +description = "Enables the has command without any pre-configured scope." +commands.allow = ["has"] + +[[permission]] +identifier = "deny-has" +description = "Denies the has command without any pre-configured scope." +commands.deny = ["has"] diff --git a/plugins/store/permissions/autogenerated/commands/keys.toml b/plugins/store/permissions/autogenerated/commands/keys.toml new file mode 100644 index 0000000..1808d0e --- /dev/null +++ b/plugins/store/permissions/autogenerated/commands/keys.toml @@ -0,0 +1,13 @@ +# Automatically generated - DO NOT EDIT! + +"$schema" = "../../schemas/schema.json" + +[[permission]] +identifier = "allow-keys" +description = "Enables the keys command without any pre-configured scope." +commands.allow = ["keys"] + +[[permission]] +identifier = "deny-keys" +description = "Denies the keys command without any pre-configured scope." +commands.deny = ["keys"] diff --git a/plugins/store/permissions/autogenerated/commands/length.toml b/plugins/store/permissions/autogenerated/commands/length.toml new file mode 100644 index 0000000..318c558 --- /dev/null +++ b/plugins/store/permissions/autogenerated/commands/length.toml @@ -0,0 +1,13 @@ +# Automatically generated - DO NOT EDIT! + +"$schema" = "../../schemas/schema.json" + +[[permission]] +identifier = "allow-length" +description = "Enables the length command without any pre-configured scope." +commands.allow = ["length"] + +[[permission]] +identifier = "deny-length" +description = "Denies the length command without any pre-configured scope." +commands.deny = ["length"] diff --git a/plugins/store/permissions/autogenerated/commands/load.toml b/plugins/store/permissions/autogenerated/commands/load.toml new file mode 100644 index 0000000..f6e47ad --- /dev/null +++ b/plugins/store/permissions/autogenerated/commands/load.toml @@ -0,0 +1,13 @@ +# Automatically generated - DO NOT EDIT! + +"$schema" = "../../schemas/schema.json" + +[[permission]] +identifier = "allow-load" +description = "Enables the load command without any pre-configured scope." +commands.allow = ["load"] + +[[permission]] +identifier = "deny-load" +description = "Denies the load command without any pre-configured scope." +commands.deny = ["load"] diff --git a/plugins/store/permissions/autogenerated/commands/reload.toml b/plugins/store/permissions/autogenerated/commands/reload.toml new file mode 100644 index 0000000..92e2525 --- /dev/null +++ b/plugins/store/permissions/autogenerated/commands/reload.toml @@ -0,0 +1,13 @@ +# Automatically generated - DO NOT EDIT! + +"$schema" = "../../schemas/schema.json" + +[[permission]] +identifier = "allow-reload" +description = "Enables the reload command without any pre-configured scope." +commands.allow = ["reload"] + +[[permission]] +identifier = "deny-reload" +description = "Denies the reload command without any pre-configured scope." +commands.deny = ["reload"] diff --git a/plugins/store/permissions/autogenerated/commands/reset.toml b/plugins/store/permissions/autogenerated/commands/reset.toml new file mode 100644 index 0000000..c32086d --- /dev/null +++ b/plugins/store/permissions/autogenerated/commands/reset.toml @@ -0,0 +1,13 @@ +# Automatically generated - DO NOT EDIT! + +"$schema" = "../../schemas/schema.json" + +[[permission]] +identifier = "allow-reset" +description = "Enables the reset command without any pre-configured scope." +commands.allow = ["reset"] + +[[permission]] +identifier = "deny-reset" +description = "Denies the reset command without any pre-configured scope." +commands.deny = ["reset"] diff --git a/plugins/store/permissions/autogenerated/commands/save.toml b/plugins/store/permissions/autogenerated/commands/save.toml new file mode 100644 index 0000000..d3e8422 --- /dev/null +++ b/plugins/store/permissions/autogenerated/commands/save.toml @@ -0,0 +1,13 @@ +# Automatically generated - DO NOT EDIT! + +"$schema" = "../../schemas/schema.json" + +[[permission]] +identifier = "allow-save" +description = "Enables the save command without any pre-configured scope." +commands.allow = ["save"] + +[[permission]] +identifier = "deny-save" +description = "Denies the save command without any pre-configured scope." +commands.deny = ["save"] diff --git a/plugins/store/permissions/autogenerated/commands/set.toml b/plugins/store/permissions/autogenerated/commands/set.toml new file mode 100644 index 0000000..1e1cc9e --- /dev/null +++ b/plugins/store/permissions/autogenerated/commands/set.toml @@ -0,0 +1,13 @@ +# Automatically generated - DO NOT EDIT! + +"$schema" = "../../schemas/schema.json" + +[[permission]] +identifier = "allow-set" +description = "Enables the set command without any pre-configured scope." +commands.allow = ["set"] + +[[permission]] +identifier = "deny-set" +description = "Denies the set command without any pre-configured scope." +commands.deny = ["set"] diff --git a/plugins/store/permissions/autogenerated/commands/values.toml b/plugins/store/permissions/autogenerated/commands/values.toml new file mode 100644 index 0000000..9343fd1 --- /dev/null +++ b/plugins/store/permissions/autogenerated/commands/values.toml @@ -0,0 +1,13 @@ +# Automatically generated - DO NOT EDIT! + +"$schema" = "../../schemas/schema.json" + +[[permission]] +identifier = "allow-values" +description = "Enables the values command without any pre-configured scope." +commands.allow = ["values"] + +[[permission]] +identifier = "deny-values" +description = "Denies the values command without any pre-configured scope." +commands.deny = ["values"] diff --git a/plugins/store/permissions/autogenerated/reference.md b/plugins/store/permissions/autogenerated/reference.md new file mode 100644 index 0000000..8980d6a --- /dev/null +++ b/plugins/store/permissions/autogenerated/reference.md @@ -0,0 +1,399 @@ +## Default Permission + +This permission set configures what kind of +operations are available from the store plugin. + +#### Granted Permissions + +All operations are enabled by default. + +#### This default permission set includes the following: + +- `allow-load` +- `allow-get-store` +- `allow-set` +- `allow-get` +- `allow-has` +- `allow-delete` +- `allow-clear` +- `allow-reset` +- `allow-keys` +- `allow-values` +- `allow-entries` +- `allow-length` +- `allow-reload` +- `allow-save` + +## Permission Table + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
IdentifierDescription
+ +`store:allow-clear` + + + +Enables the clear command without any pre-configured scope. + +
+ +`store:deny-clear` + + + +Denies the clear command without any pre-configured scope. + +
+ +`store:allow-delete` + + + +Enables the delete command without any pre-configured scope. + +
+ +`store:deny-delete` + + + +Denies the delete command without any pre-configured scope. + +
+ +`store:allow-entries` + + + +Enables the entries command without any pre-configured scope. + +
+ +`store:deny-entries` + + + +Denies the entries command without any pre-configured scope. + +
+ +`store:allow-get` + + + +Enables the get command without any pre-configured scope. + +
+ +`store:deny-get` + + + +Denies the get command without any pre-configured scope. + +
+ +`store:allow-get-store` + + + +Enables the get_store command without any pre-configured scope. + +
+ +`store:deny-get-store` + + + +Denies the get_store command without any pre-configured scope. + +
+ +`store:allow-has` + + + +Enables the has command without any pre-configured scope. + +
+ +`store:deny-has` + + + +Denies the has command without any pre-configured scope. + +
+ +`store:allow-keys` + + + +Enables the keys command without any pre-configured scope. + +
+ +`store:deny-keys` + + + +Denies the keys command without any pre-configured scope. + +
+ +`store:allow-length` + + + +Enables the length command without any pre-configured scope. + +
+ +`store:deny-length` + + + +Denies the length command without any pre-configured scope. + +
+ +`store:allow-load` + + + +Enables the load command without any pre-configured scope. + +
+ +`store:deny-load` + + + +Denies the load command without any pre-configured scope. + +
+ +`store:allow-reload` + + + +Enables the reload command without any pre-configured scope. + +
+ +`store:deny-reload` + + + +Denies the reload command without any pre-configured scope. + +
+ +`store:allow-reset` + + + +Enables the reset command without any pre-configured scope. + +
+ +`store:deny-reset` + + + +Denies the reset command without any pre-configured scope. + +
+ +`store:allow-save` + + + +Enables the save command without any pre-configured scope. + +
+ +`store:deny-save` + + + +Denies the save command without any pre-configured scope. + +
+ +`store:allow-set` + + + +Enables the set command without any pre-configured scope. + +
+ +`store:deny-set` + + + +Denies the set command without any pre-configured scope. + +
+ +`store:allow-values` + + + +Enables the values command without any pre-configured scope. + +
+ +`store:deny-values` + + + +Denies the values command without any pre-configured scope. + +
diff --git a/plugins/store/permissions/default.toml b/plugins/store/permissions/default.toml new file mode 100644 index 0000000..3a3e4b3 --- /dev/null +++ b/plugins/store/permissions/default.toml @@ -0,0 +1,28 @@ +"$schema" = "schemas/schema.json" + +[default] +description = """ +This permission set configures what kind of +operations are available from the store plugin. + +#### Granted Permissions + +All operations are enabled by default. + +""" +permissions = [ + "allow-load", + "allow-get-store", + "allow-set", + "allow-get", + "allow-has", + "allow-delete", + "allow-clear", + "allow-reset", + "allow-keys", + "allow-values", + "allow-entries", + "allow-length", + "allow-reload", + "allow-save", +] diff --git a/plugins/store/permissions/schemas/schema.json b/plugins/store/permissions/schemas/schema.json new file mode 100644 index 0000000..af47518 --- /dev/null +++ b/plugins/store/permissions/schemas/schema.json @@ -0,0 +1,474 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "PermissionFile", + "description": "Permission file that can define a default permission, a set of permissions or a list of inlined permissions.", + "type": "object", + "properties": { + "default": { + "description": "The default permission set for the plugin", + "anyOf": [ + { + "$ref": "#/definitions/DefaultPermission" + }, + { + "type": "null" + } + ] + }, + "set": { + "description": "A list of permissions sets defined", + "type": "array", + "items": { + "$ref": "#/definitions/PermissionSet" + } + }, + "permission": { + "description": "A list of inlined permissions", + "default": [], + "type": "array", + "items": { + "$ref": "#/definitions/Permission" + } + } + }, + "definitions": { + "DefaultPermission": { + "description": "The default permission set of the plugin.\n\nWorks similarly to a permission with the \"default\" identifier.", + "type": "object", + "required": [ + "permissions" + ], + "properties": { + "version": { + "description": "The version of the permission.", + "type": [ + "integer", + "null" + ], + "format": "uint64", + "minimum": 1.0 + }, + "description": { + "description": "Human-readable description of what the permission does. Tauri convention is to use `

` headings in markdown content for Tauri documentation generation purposes.", + "type": [ + "string", + "null" + ] + }, + "permissions": { + "description": "All permissions this set contains.", + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "PermissionSet": { + "description": "A set of direct permissions grouped together under a new name.", + "type": "object", + "required": [ + "description", + "identifier", + "permissions" + ], + "properties": { + "identifier": { + "description": "A unique identifier for the permission.", + "type": "string" + }, + "description": { + "description": "Human-readable description of what the permission does.", + "type": "string" + }, + "permissions": { + "description": "All permissions this set contains.", + "type": "array", + "items": { + "$ref": "#/definitions/PermissionKind" + } + } + } + }, + "Permission": { + "description": "Descriptions of explicit privileges of commands.\n\nIt can enable commands to be accessible in the frontend of the application.\n\nIf the scope is defined it can be used to fine grain control the access of individual or multiple commands.", + "type": "object", + "required": [ + "identifier" + ], + "properties": { + "version": { + "description": "The version of the permission.", + "type": [ + "integer", + "null" + ], + "format": "uint64", + "minimum": 1.0 + }, + "identifier": { + "description": "A unique identifier for the permission.", + "type": "string" + }, + "description": { + "description": "Human-readable description of what the permission does. Tauri internal convention is to use `

` headings in markdown content for Tauri documentation generation purposes.", + "type": [ + "string", + "null" + ] + }, + "commands": { + "description": "Allowed or denied commands when using this permission.", + "default": { + "allow": [], + "deny": [] + }, + "allOf": [ + { + "$ref": "#/definitions/Commands" + } + ] + }, + "scope": { + "description": "Allowed or denied scoped when using this permission.", + "allOf": [ + { + "$ref": "#/definitions/Scopes" + } + ] + }, + "platforms": { + "description": "Target platforms this permission applies. By default all platforms are affected by this permission.", + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/Target" + } + } + } + }, + "Commands": { + "description": "Allowed and denied commands inside a permission.\n\nIf two commands clash inside of `allow` and `deny`, it should be denied by default.", + "type": "object", + "properties": { + "allow": { + "description": "Allowed command.", + "default": [], + "type": "array", + "items": { + "type": "string" + } + }, + "deny": { + "description": "Denied command, which takes priority.", + "default": [], + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "Scopes": { + "description": "An argument for fine grained behavior control of Tauri commands.\n\nIt can be of any serde serializable type and is used to allow or prevent certain actions inside a Tauri command. The configured scope is passed to the command and will be enforced by the command implementation.\n\n## Example\n\n```json { \"allow\": [{ \"path\": \"$HOME/**\" }], \"deny\": [{ \"path\": \"$HOME/secret.txt\" }] } ```", + "type": "object", + "properties": { + "allow": { + "description": "Data that defines what is allowed by the scope.", + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/Value" + } + }, + "deny": { + "description": "Data that defines what is denied by the scope. This should be prioritized by validation logic.", + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/Value" + } + } + } + }, + "Value": { + "description": "All supported ACL values.", + "anyOf": [ + { + "description": "Represents a null JSON value.", + "type": "null" + }, + { + "description": "Represents a [`bool`].", + "type": "boolean" + }, + { + "description": "Represents a valid ACL [`Number`].", + "allOf": [ + { + "$ref": "#/definitions/Number" + } + ] + }, + { + "description": "Represents a [`String`].", + "type": "string" + }, + { + "description": "Represents a list of other [`Value`]s.", + "type": "array", + "items": { + "$ref": "#/definitions/Value" + } + }, + { + "description": "Represents a map of [`String`] keys to [`Value`]s.", + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/Value" + } + } + ] + }, + "Number": { + "description": "A valid ACL number.", + "anyOf": [ + { + "description": "Represents an [`i64`].", + "type": "integer", + "format": "int64" + }, + { + "description": "Represents a [`f64`].", + "type": "number", + "format": "double" + } + ] + }, + "Target": { + "description": "Platform target.", + "oneOf": [ + { + "description": "MacOS.", + "type": "string", + "enum": [ + "macOS" + ] + }, + { + "description": "Windows.", + "type": "string", + "enum": [ + "windows" + ] + }, + { + "description": "Linux.", + "type": "string", + "enum": [ + "linux" + ] + }, + { + "description": "Android.", + "type": "string", + "enum": [ + "android" + ] + }, + { + "description": "iOS.", + "type": "string", + "enum": [ + "iOS" + ] + } + ] + }, + "PermissionKind": { + "type": "string", + "oneOf": [ + { + "description": "Enables the clear command without any pre-configured scope.", + "type": "string", + "const": "allow-clear", + "markdownDescription": "Enables the clear command without any pre-configured scope." + }, + { + "description": "Denies the clear command without any pre-configured scope.", + "type": "string", + "const": "deny-clear", + "markdownDescription": "Denies the clear command without any pre-configured scope." + }, + { + "description": "Enables the delete command without any pre-configured scope.", + "type": "string", + "const": "allow-delete", + "markdownDescription": "Enables the delete command without any pre-configured scope." + }, + { + "description": "Denies the delete command without any pre-configured scope.", + "type": "string", + "const": "deny-delete", + "markdownDescription": "Denies the delete command without any pre-configured scope." + }, + { + "description": "Enables the entries command without any pre-configured scope.", + "type": "string", + "const": "allow-entries", + "markdownDescription": "Enables the entries command without any pre-configured scope." + }, + { + "description": "Denies the entries command without any pre-configured scope.", + "type": "string", + "const": "deny-entries", + "markdownDescription": "Denies the entries command without any pre-configured scope." + }, + { + "description": "Enables the get command without any pre-configured scope.", + "type": "string", + "const": "allow-get", + "markdownDescription": "Enables the get command without any pre-configured scope." + }, + { + "description": "Denies the get command without any pre-configured scope.", + "type": "string", + "const": "deny-get", + "markdownDescription": "Denies the get command without any pre-configured scope." + }, + { + "description": "Enables the get_store command without any pre-configured scope.", + "type": "string", + "const": "allow-get-store", + "markdownDescription": "Enables the get_store command without any pre-configured scope." + }, + { + "description": "Denies the get_store command without any pre-configured scope.", + "type": "string", + "const": "deny-get-store", + "markdownDescription": "Denies the get_store command without any pre-configured scope." + }, + { + "description": "Enables the has command without any pre-configured scope.", + "type": "string", + "const": "allow-has", + "markdownDescription": "Enables the has command without any pre-configured scope." + }, + { + "description": "Denies the has command without any pre-configured scope.", + "type": "string", + "const": "deny-has", + "markdownDescription": "Denies the has command without any pre-configured scope." + }, + { + "description": "Enables the keys command without any pre-configured scope.", + "type": "string", + "const": "allow-keys", + "markdownDescription": "Enables the keys command without any pre-configured scope." + }, + { + "description": "Denies the keys command without any pre-configured scope.", + "type": "string", + "const": "deny-keys", + "markdownDescription": "Denies the keys command without any pre-configured scope." + }, + { + "description": "Enables the length command without any pre-configured scope.", + "type": "string", + "const": "allow-length", + "markdownDescription": "Enables the length command without any pre-configured scope." + }, + { + "description": "Denies the length command without any pre-configured scope.", + "type": "string", + "const": "deny-length", + "markdownDescription": "Denies the length command without any pre-configured scope." + }, + { + "description": "Enables the load command without any pre-configured scope.", + "type": "string", + "const": "allow-load", + "markdownDescription": "Enables the load command without any pre-configured scope." + }, + { + "description": "Denies the load command without any pre-configured scope.", + "type": "string", + "const": "deny-load", + "markdownDescription": "Denies the load command without any pre-configured scope." + }, + { + "description": "Enables the reload command without any pre-configured scope.", + "type": "string", + "const": "allow-reload", + "markdownDescription": "Enables the reload command without any pre-configured scope." + }, + { + "description": "Denies the reload command without any pre-configured scope.", + "type": "string", + "const": "deny-reload", + "markdownDescription": "Denies the reload command without any pre-configured scope." + }, + { + "description": "Enables the reset command without any pre-configured scope.", + "type": "string", + "const": "allow-reset", + "markdownDescription": "Enables the reset command without any pre-configured scope." + }, + { + "description": "Denies the reset command without any pre-configured scope.", + "type": "string", + "const": "deny-reset", + "markdownDescription": "Denies the reset command without any pre-configured scope." + }, + { + "description": "Enables the save command without any pre-configured scope.", + "type": "string", + "const": "allow-save", + "markdownDescription": "Enables the save command without any pre-configured scope." + }, + { + "description": "Denies the save command without any pre-configured scope.", + "type": "string", + "const": "deny-save", + "markdownDescription": "Denies the save command without any pre-configured scope." + }, + { + "description": "Enables the set command without any pre-configured scope.", + "type": "string", + "const": "allow-set", + "markdownDescription": "Enables the set command without any pre-configured scope." + }, + { + "description": "Denies the set command without any pre-configured scope.", + "type": "string", + "const": "deny-set", + "markdownDescription": "Denies the set command without any pre-configured scope." + }, + { + "description": "Enables the values command without any pre-configured scope.", + "type": "string", + "const": "allow-values", + "markdownDescription": "Enables the values command without any pre-configured scope." + }, + { + "description": "Denies the values command without any pre-configured scope.", + "type": "string", + "const": "deny-values", + "markdownDescription": "Denies the values command without any pre-configured scope." + }, + { + "description": "This permission set configures what kind of\noperations are available from the store plugin.\n\n#### Granted Permissions\n\nAll operations are enabled by default.\n\n\n#### This default permission set includes:\n\n- `allow-load`\n- `allow-get-store`\n- `allow-set`\n- `allow-get`\n- `allow-has`\n- `allow-delete`\n- `allow-clear`\n- `allow-reset`\n- `allow-keys`\n- `allow-values`\n- `allow-entries`\n- `allow-length`\n- `allow-reload`\n- `allow-save`", + "type": "string", + "const": "default", + "markdownDescription": "This permission set configures what kind of\noperations are available from the store plugin.\n\n#### Granted Permissions\n\nAll operations are enabled by default.\n\n\n#### This default permission set includes:\n\n- `allow-load`\n- `allow-get-store`\n- `allow-set`\n- `allow-get`\n- `allow-has`\n- `allow-delete`\n- `allow-clear`\n- `allow-reset`\n- `allow-keys`\n- `allow-values`\n- `allow-entries`\n- `allow-length`\n- `allow-reload`\n- `allow-save`" + } + ] + } + } +} \ No newline at end of file diff --git a/plugins/store/rollup.config.js b/plugins/store/rollup.config.js new file mode 100644 index 0000000..1f349ec --- /dev/null +++ b/plugins/store/rollup.config.js @@ -0,0 +1,7 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +import { createConfig } from '../../shared/rollup.config.js' + +export default createConfig() diff --git a/plugins/store/src/error.rs b/plugins/store/src/error.rs new file mode 100644 index 0000000..29abf9d --- /dev/null +++ b/plugins/store/src/error.rs @@ -0,0 +1,47 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use serde::{Serialize, Serializer}; + +/// Alias for a [`Result`](std::result::Result) with the error type [`Error`]. +pub type Result = std::result::Result; + +/// The error types. +#[derive(thiserror::Error, Debug)] +#[non_exhaustive] +pub enum Error { + /// The store contents could not be serialized by the configured [`SerializeFn`](crate::SerializeFn). + #[error("Failed to serialize store. {0}")] + Serialize(Box), + /// The store contents could not be deserialized by the configured [`DeserializeFn`](crate::DeserializeFn). + #[error("Failed to deserialize store. {0}")] + Deserialize(Box), + /// JSON error. + #[error(transparent)] + Json(#[from] serde_json::Error), + /// IO error. + #[error(transparent)] + Io(#[from] std::io::Error), + // /// Store already exists + // #[error("Store at \"{0}\" already exists")] + // AlreadyExists(PathBuf), + /// Serialize function not found + #[error("Serialize Function \"{0}\" not found")] + SerializeFunctionNotFound(String), + /// Deserialize function not found + #[error("Deserialize Function \"{0}\" not found")] + DeserializeFunctionNotFound(String), + /// Some Tauri API failed + #[error(transparent)] + Tauri(#[from] tauri::Error), +} + +impl Serialize for Error { + fn serialize(&self, serializer: S) -> std::result::Result + where + S: Serializer, + { + serializer.serialize_str(self.to_string().as_ref()) + } +} diff --git a/plugins/store/src/lib.rs b/plugins/store/src/lib.rs new file mode 100644 index 0000000..4a49dda --- /dev/null +++ b/plugins/store/src/lib.rs @@ -0,0 +1,483 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +//! Simple, persistent key-value store. + +#![doc( + html_logo_url = "https://github.com/tauri-apps/tauri/raw/dev/app-icon.png", + html_favicon_url = "https://github.com/tauri-apps/tauri/raw/dev/app-icon.png" +)] + +pub use error::{Error, Result}; +use serde::{Deserialize, Serialize}; +pub use serde_json::Value as JsonValue; +use std::{ + collections::HashMap, + path::{Path, PathBuf}, + sync::{Arc, RwLock}, + time::Duration, +}; +pub use store::{resolve_store_path, DeserializeFn, SerializeFn, Store, StoreBuilder}; +use tauri::{ + plugin::{self, TauriPlugin}, + AppHandle, Manager, ResourceId, RunEvent, Runtime, State, +}; + +mod error; +mod store; + +#[derive(Serialize, Clone)] +#[serde(rename_all = "camelCase")] +struct ChangePayload<'a> { + path: &'a Path, + resource_id: Option, + key: &'a str, + value: Option<&'a JsonValue>, + exists: bool, +} + +#[derive(Debug)] +struct StoreState { + stores: Arc>>, + serialize_fns: HashMap, + deserialize_fns: HashMap, + default_serialize: SerializeFn, + default_deserialize: DeserializeFn, +} + +#[derive(Serialize, Deserialize)] +#[serde(untagged)] +enum AutoSave { + DebounceDuration(u64), + Bool(bool), +} + +#[derive(Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +struct LoadStoreOptions { + defaults: Option>, + auto_save: Option, + serialize_fn_name: Option, + deserialize_fn_name: Option, + #[serde(default)] + create_new: bool, + #[serde(default)] + override_defaults: bool, +} + +fn builder( + app: AppHandle, + store_state: State<'_, StoreState>, + path: PathBuf, + options: Option, +) -> Result> { + let mut builder = app.store_builder(path); + + let Some(options) = options else { + return Ok(builder); + }; + + if let Some(defaults) = options.defaults { + builder = builder.defaults(defaults); + } + + if let Some(auto_save) = options.auto_save { + match auto_save { + AutoSave::DebounceDuration(duration) => { + builder = builder.auto_save(Duration::from_millis(duration)); + } + AutoSave::Bool(false) => { + builder = builder.disable_auto_save(); + } + _ => {} + } + } + + if let Some(serialize_fn_name) = options.serialize_fn_name { + let serialize_fn = store_state + .serialize_fns + .get(&serialize_fn_name) + .ok_or_else(|| crate::Error::SerializeFunctionNotFound(serialize_fn_name))?; + builder = builder.serialize(*serialize_fn); + } + + if let Some(deserialize_fn_name) = options.deserialize_fn_name { + let deserialize_fn = store_state + .deserialize_fns + .get(&deserialize_fn_name) + .ok_or_else(|| crate::Error::DeserializeFunctionNotFound(deserialize_fn_name))?; + builder = builder.deserialize(*deserialize_fn); + } + + if options.create_new { + builder = builder.create_new(); + } + + if options.override_defaults { + builder = builder.override_defaults(); + } + + Ok(builder) +} + +#[tauri::command] +async fn load( + app: AppHandle, + store_state: State<'_, StoreState>, + path: PathBuf, + options: Option, +) -> Result { + let builder = builder(app, store_state, path, options)?; + let (_, rid) = builder.build_inner()?; + Ok(rid) +} + +#[tauri::command] +async fn get_store( + app: AppHandle, + store_state: State<'_, StoreState>, + path: PathBuf, +) -> Result> { + let stores = store_state.stores.read().unwrap(); + Ok(stores.get(&resolve_store_path(&app, path)?).copied()) +} + +#[tauri::command] +async fn set( + app: AppHandle, + rid: ResourceId, + key: String, + value: JsonValue, +) -> Result<()> { + let store = app.resources_table().get::>(rid)?; + store.set(key, value); + Ok(()) +} + +#[tauri::command] +async fn get( + app: AppHandle, + rid: ResourceId, + key: String, +) -> Result<(Option, bool)> { + let store = app.resources_table().get::>(rid)?; + let value = store.get(key); + let exists = value.is_some(); + Ok((value, exists)) +} + +#[tauri::command] +async fn has(app: AppHandle, rid: ResourceId, key: String) -> Result { + let store = app.resources_table().get::>(rid)?; + Ok(store.has(key)) +} + +#[tauri::command] +async fn delete(app: AppHandle, rid: ResourceId, key: String) -> Result { + let store = app.resources_table().get::>(rid)?; + Ok(store.delete(key)) +} + +#[tauri::command] +async fn clear(app: AppHandle, rid: ResourceId) -> Result<()> { + let store = app.resources_table().get::>(rid)?; + store.clear(); + Ok(()) +} + +#[tauri::command] +async fn reset(app: AppHandle, rid: ResourceId) -> Result<()> { + let store = app.resources_table().get::>(rid)?; + store.reset(); + Ok(()) +} + +#[tauri::command] +async fn keys(app: AppHandle, rid: ResourceId) -> Result> { + let store = app.resources_table().get::>(rid)?; + Ok(store.keys()) +} + +#[tauri::command] +async fn values(app: AppHandle, rid: ResourceId) -> Result> { + let store = app.resources_table().get::>(rid)?; + Ok(store.values()) +} + +#[tauri::command] +async fn entries( + app: AppHandle, + rid: ResourceId, +) -> Result> { + let store = app.resources_table().get::>(rid)?; + Ok(store.entries()) +} + +#[tauri::command] +async fn length(app: AppHandle, rid: ResourceId) -> Result { + let store = app.resources_table().get::>(rid)?; + Ok(store.length()) +} + +#[tauri::command] +async fn reload( + app: AppHandle, + rid: ResourceId, + ignore_defaults: Option, +) -> Result<()> { + let store = app.resources_table().get::>(rid)?; + if ignore_defaults.unwrap_or_default() { + store.reload_ignore_defaults() + } else { + store.reload() + } +} + +#[tauri::command] +async fn save(app: AppHandle, rid: ResourceId) -> Result<()> { + let store = app.resources_table().get::>(rid)?; + store.save() +} + +/// Extension trait to access the store APIs on a [`Manager`] such as `App`, `AppHandle`, +/// `WebviewWindow` or `Window`. +/// +/// The plugin must be registered with [`Builder::build`] for these methods to work, +/// as they rely on the state it manages. +pub trait StoreExt { + /// Create a store or load an existing store with default settings at the given path. + /// + /// If the store is already loaded, its instance is automatically returned. + /// + /// # Examples + /// + /// ``` + /// use tauri_plugin_store::StoreExt; + /// + /// tauri::Builder::default() + /// .plugin(tauri_plugin_store::Builder::default().build()) + /// .setup(|app| { + /// let store = app.store("my-store")?; + /// Ok(()) + /// }); + /// ``` + fn store(&self, path: impl AsRef) -> Result>>; + /// Get a store builder. + /// + /// The builder can be used to configure the store. + /// To use the default settings see [`Self::store`]. + /// + /// # Examples + /// + /// ``` + /// use tauri_plugin_store::StoreExt; + /// use std::time::Duration; + /// + /// tauri::Builder::default() + /// .plugin(tauri_plugin_store::Builder::default().build()) + /// .setup(|app| { + /// let store = app.store_builder("users.json").auto_save(Duration::from_secs(1)).build()?; + /// Ok(()) + /// }); + /// ``` + fn store_builder(&self, path: impl AsRef) -> StoreBuilder; + /// Get a handle of an already loaded store. + /// + /// If the store is not loaded or does not exist, it returns `None`. + /// + /// Note that using this function can cause race conditions if you fallback to creating or loading the store, + /// so you should consider using [`Self::store`] if you are not sure if the store is loaded or not. + /// + /// # Examples + /// + /// ``` + /// use tauri_plugin_store::StoreExt; + /// + /// tauri::Builder::default() + /// .plugin(tauri_plugin_store::Builder::default().build()) + /// .setup(|app| { + /// let store = if let Some(s) = app.get_store("store.json") { + /// s + /// } else { + /// // this is not thread safe; if another thread is doing the same load/create, + /// // there will be a race condition; in this case we could remove the get_store + /// // and only run app.store() as it will return the existing store if it has been loaded + /// app.store("store.json")? + /// }; + /// Ok(()) + /// }); + /// ``` + fn get_store(&self, path: impl AsRef) -> Option>>; +} + +impl> StoreExt for T { + fn store(&self, path: impl AsRef) -> Result>> { + StoreBuilder::new(self.app_handle(), path).build() + } + + fn store_builder(&self, path: impl AsRef) -> StoreBuilder { + StoreBuilder::new(self.app_handle(), path) + } + + fn get_store(&self, path: impl AsRef) -> Option>> { + let collection = self.state::(); + let stores = collection.stores.read().unwrap(); + stores + .get(&resolve_store_path(self.app_handle(), path.as_ref()).ok()?) + .and_then(|rid| self.resources_table().get(*rid).ok()) + } +} + +fn default_serialize( + cache: &HashMap, +) -> std::result::Result, Box> { + Ok(serde_json::to_vec_pretty(&cache)?) +} + +fn default_deserialize( + bytes: &[u8], +) -> std::result::Result, Box> { + serde_json::from_slice(bytes).map_err(Into::into) +} + +/// Builder for the store plugin. +/// +/// It is used to register custom serialize and deserialize functions the frontend can select by +/// name when loading a store, and to change the functions used by default (pretty printed JSON). +/// +/// # Examples +/// +/// ``` +/// tauri::Builder::default() +/// .plugin(tauri_plugin_store::Builder::default().build()); +/// ``` +pub struct Builder { + serialize_fns: HashMap, + deserialize_fns: HashMap, + default_serialize: SerializeFn, + default_deserialize: DeserializeFn, +} + +impl Default for Builder { + fn default() -> Self { + Self { + serialize_fns: Default::default(), + deserialize_fns: Default::default(), + default_serialize, + default_deserialize, + } + } +} + +impl Builder { + /// Creates a new builder using the default serialize and deserialize functions, + /// which read and write pretty printed JSON. + /// + /// This is the same as [`Builder::default`]. + pub fn new() -> Self { + Self::default() + } + + /// Register a serialize function to access it from the JavaScript side + /// + /// # Examples + /// + /// ``` + /// fn no_pretty_json( + /// cache: &std::collections::HashMap, + /// ) -> Result, Box> { + /// Ok(serde_json::to_vec(&cache)?) + /// } + /// + /// tauri::Builder::default() + /// .plugin( + /// tauri_plugin_store::Builder::default() + /// .register_serialize_fn("no-pretty-json".to_owned(), no_pretty_json) + /// .build(), + /// ); + /// ``` + pub fn register_serialize_fn(mut self, name: String, serialize_fn: SerializeFn) -> Self { + self.serialize_fns.insert(name, serialize_fn); + self + } + + /// Register a deserialize function to access it from the JavaScript side + pub fn register_deserialize_fn(mut self, name: String, deserialize_fn: DeserializeFn) -> Self { + self.deserialize_fns.insert(name, deserialize_fn); + self + } + + /// Use this serialize function for stores by default + /// + /// # Examples + /// + /// ``` + /// fn no_pretty_json( + /// cache: &std::collections::HashMap, + /// ) -> Result, Box> { + /// Ok(serde_json::to_vec(&cache)?) + /// } + /// + /// tauri::Builder::default() + /// .plugin( + /// tauri_plugin_store::Builder::default() + /// .default_serialize_fn(no_pretty_json) + /// .build(), + /// ); + /// ``` + pub fn default_serialize_fn(mut self, serialize_fn: SerializeFn) -> Self { + self.default_serialize = serialize_fn; + self + } + + /// Use this deserialize function for stores by default + pub fn default_deserialize_fn(mut self, deserialize_fn: DeserializeFn) -> Self { + self.default_deserialize = deserialize_fn; + self + } + + /// Builds the plugin. + /// + /// # Examples + /// + /// ``` + /// tauri::Builder::default() + /// .plugin(tauri_plugin_store::Builder::default().build()) + /// .setup(|app| { + /// let store = tauri_plugin_store::StoreBuilder::new(app, "store.bin").build()?; + /// Ok(()) + /// }); + /// ``` + pub fn build(self) -> TauriPlugin { + plugin::Builder::new("store") + .invoke_handler(tauri::generate_handler![ + load, get_store, set, get, has, delete, clear, reset, keys, values, length, + entries, reload, save, + ]) + .setup(move |app_handle, _api| { + app_handle.manage(StoreState { + stores: Arc::new(RwLock::new(HashMap::new())), + serialize_fns: self.serialize_fns, + deserialize_fns: self.deserialize_fns, + default_serialize: self.default_serialize, + default_deserialize: self.default_deserialize, + }); + Ok(()) + }) + .on_event(|app_handle, event| { + if let RunEvent::Exit = event { + let collection = app_handle.state::(); + let stores = collection.stores.read().unwrap(); + for (path, rid) in stores.iter() { + if let Ok(store) = app_handle.resources_table().get::>(*rid) { + if let Err(err) = store.save() { + tracing::error!("failed to save store {path:?} with error {err:?}"); + } + } + } + } + }) + .build() + } +} diff --git a/plugins/store/src/store.rs b/plugins/store/src/store.rs new file mode 100644 index 0000000..b13a34a --- /dev/null +++ b/plugins/store/src/store.rs @@ -0,0 +1,638 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use crate::{ChangePayload, StoreState}; +use serde_json::Value as JsonValue; +use std::{ + collections::HashMap, + fs, + path::{Path, PathBuf}, + sync::{Arc, Mutex}, + time::Duration, +}; +use tauri::{path::BaseDirectory, AppHandle, Emitter, Manager, Resource, ResourceId, Runtime}; +use tokio::{ + select, + sync::mpsc::{unbounded_channel, UnboundedSender}, + time::sleep, +}; + +/// Function used to serialize the store cache to the bytes written to the store file. +/// +/// The default implementation writes pretty printed JSON. +pub type SerializeFn = + fn(&HashMap) -> Result, Box>; +/// Function used to deserialize the bytes read from the store file into the store cache. +/// +/// The default implementation parses JSON. +pub type DeserializeFn = + fn(&[u8]) -> Result, Box>; + +/// Resolves the path of a store file, relative to the app data directory +/// ([`BaseDirectory::AppData`]). +/// +/// This is the path the [`Store`] created with the given `path` reads from and writes to. +/// +/// # Errors +/// +/// Returns an error if the app data directory cannot be resolved. +pub fn resolve_store_path( + app: &AppHandle, + path: impl AsRef, +) -> crate::Result { + Ok(dunce::simplified(&app.path().resolve(path, BaseDirectory::AppData)?).to_path_buf()) +} + +/// Builds a [`Store`] +pub struct StoreBuilder { + app: AppHandle, + path: PathBuf, + defaults: Option>, + serialize_fn: SerializeFn, + deserialize_fn: DeserializeFn, + auto_save: Option, + create_new: bool, + override_defaults: bool, +} + +impl StoreBuilder { + /// Creates a new [`StoreBuilder`]. + /// + /// # Examples + /// ``` + /// tauri::Builder::default() + /// .plugin(tauri_plugin_store::Builder::default().build()) + /// .setup(|app| { + /// let builder = tauri_plugin_store::StoreBuilder::new(app, "store.bin"); + /// Ok(()) + /// }); + /// ``` + pub fn new, P: AsRef>(manager: &M, path: P) -> Self { + let app = manager.app_handle().clone(); + let state = app.state::(); + let serialize_fn = state.default_serialize; + let deserialize_fn = state.default_deserialize; + Self { + app, + path: path.as_ref().to_path_buf(), + defaults: None, + serialize_fn, + deserialize_fn, + auto_save: Some(Duration::from_millis(100)), + create_new: false, + override_defaults: false, + } + } + + /// Inserts a default key-value pair. + /// + /// # Examples + /// ``` + /// tauri::Builder::default() + /// .plugin(tauri_plugin_store::Builder::default().build()) + /// .setup(|app| { + /// let mut defaults = std::collections::HashMap::new(); + /// defaults.insert("foo".to_string(), "bar".into()); + /// + /// let store = tauri_plugin_store::StoreBuilder::new(app, "store.bin") + /// .defaults(defaults) + /// .build()?; + /// Ok(()) + /// }); + /// ``` + pub fn defaults(mut self, defaults: HashMap) -> Self { + self.defaults = Some(defaults); + self + } + + /// Inserts multiple default key-value pairs. + /// + /// # Examples + /// ``` + /// tauri::Builder::default() + /// .plugin(tauri_plugin_store::Builder::default().build()) + /// .setup(|app| { + /// let store = tauri_plugin_store::StoreBuilder::new(app, "store.bin") + /// .default("foo".to_string(), "bar") + /// .build()?; + /// Ok(()) + /// }); + /// ``` + pub fn default(mut self, key: impl Into, value: impl Into) -> Self { + let key = key.into(); + let value = value.into(); + self.defaults + .get_or_insert(HashMap::new()) + .insert(key, value); + self + } + + /// Defines a custom serialization function. + /// + /// # Examples + /// ``` + /// tauri::Builder::default() + /// .plugin(tauri_plugin_store::Builder::default().build()) + /// .setup(|app| { + /// let store = tauri_plugin_store::StoreBuilder::new(app, "store.json") + /// .serialize(|cache| serde_json::to_vec(&cache).map_err(Into::into)) + /// .build()?; + /// Ok(()) + /// }); + /// ``` + pub fn serialize(mut self, serialize: SerializeFn) -> Self { + self.serialize_fn = serialize; + self + } + + /// Defines a custom deserialization function + /// + /// # Examples + /// ``` + /// tauri::Builder::default() + /// .plugin(tauri_plugin_store::Builder::default().build()) + /// .setup(|app| { + /// let store = tauri_plugin_store::StoreBuilder::new(app, "store.json") + /// .deserialize(|bytes| serde_json::from_slice(&bytes).map_err(Into::into)) + /// .build()?; + /// Ok(()) + /// }); + /// ``` + pub fn deserialize(mut self, deserialize: DeserializeFn) -> Self { + self.deserialize_fn = deserialize; + self + } + + /// Auto save on modified with a debounce duration + /// + /// # Examples + /// ``` + /// tauri::Builder::default() + /// .plugin(tauri_plugin_store::Builder::default().build()) + /// .setup(|app| { + /// let store = tauri_plugin_store::StoreBuilder::new(app, "store.json") + /// .auto_save(std::time::Duration::from_millis(100)) + /// .build()?; + /// Ok(()) + /// }); + /// ``` + pub fn auto_save(mut self, debounce_duration: Duration) -> Self { + self.auto_save = Some(debounce_duration); + self + } + + /// Disable auto save on modified with a debounce duration. + pub fn disable_auto_save(mut self) -> Self { + self.auto_save = None; + self + } + + /// Force create a new store with default values even if it already exists. + pub fn create_new(mut self) -> Self { + self.create_new = true; + self + } + + /// Override the store values when creating the store, ignoring defaults. + pub fn override_defaults(mut self) -> Self { + self.override_defaults = true; + self + } + + pub(crate) fn build_inner(mut self) -> crate::Result<(Arc>, ResourceId)> { + let stores = self.app.state::().stores.clone(); + let mut stores = stores.write().unwrap(); + + self.path = resolve_store_path(&self.app, self.path)?; + + if self.create_new { + if let Some(rid) = stores.remove(&self.path) { + let _ = self.app.resources_table().take::>(rid); + } + } else if let Some(rid) = stores.get(&self.path) { + // The resource id we stored can be invalid due to + // the resource table getting modified by an external source + // (e.g. `App::cleanup_before_exit` > `manager.resources_table.clear()`) + return Ok((self.app.resources_table().get(*rid)?, *rid)); + } + + // if stores.contains_key(&self.path) { + // return Err(crate::Error::AlreadyExists(self.path)); + // } + + let mut store_inner = StoreInner::new( + self.app.clone(), + self.path.clone(), + self.defaults.take(), + self.serialize_fn, + self.deserialize_fn, + ); + + if !self.create_new { + if self.override_defaults { + let _ = store_inner.load_ignore_defaults(); + } else { + let _ = store_inner.load(); + } + } + + let store = Store { + auto_save: self.auto_save, + auto_save_debounce_sender: Arc::new(Mutex::new(None)), + store: Arc::new(Mutex::new(store_inner)), + }; + + let store = Arc::new(store); + let rid = self.app.resources_table().add_arc(store.clone()); + stores.insert(self.path, rid); + + Ok((store, rid)) + } + + /// Load the existing store with the same path or creates a new [`Store`]. + /// + /// If a store with the same path has already been loaded its instance is returned. + /// + /// # Examples + /// ``` + /// tauri::Builder::default() + /// .plugin(tauri_plugin_store::Builder::default().build()) + /// .setup(|app| { + /// let store = tauri_plugin_store::StoreBuilder::new(app, "store.json").build(); + /// Ok(()) + /// }); + /// ``` + pub fn build(self) -> crate::Result>> { + let (store, _) = self.build_inner()?; + Ok(store) + } +} + +enum AutoSaveMessage { + Reset, + Cancel, +} + +#[derive(Clone)] +struct StoreInner { + app: AppHandle, + path: PathBuf, + cache: HashMap, + defaults: Option>, + serialize_fn: SerializeFn, + deserialize_fn: DeserializeFn, +} + +impl StoreInner { + fn new( + app: AppHandle, + path: PathBuf, + defaults: Option>, + serialize_fn: SerializeFn, + deserialize_fn: DeserializeFn, + ) -> Self { + Self { + app, + path, + cache: defaults.clone().unwrap_or_default(), + defaults, + serialize_fn, + deserialize_fn, + } + } + + /// Saves the store to disk at the store's `path`. + pub fn save(&self) -> crate::Result<()> { + fs::create_dir_all(self.path.parent().expect("invalid store path"))?; + + let bytes = (self.serialize_fn)(&self.cache).map_err(crate::Error::Serialize)?; + fs::write(&self.path, bytes)?; + + Ok(()) + } + + /// Update the store from the on-disk state + /// + /// Note: This method loads the data and merges it with the current store + pub fn load(&mut self) -> crate::Result<()> { + let bytes = fs::read(&self.path)?; + + self.cache + .extend((self.deserialize_fn)(&bytes).map_err(crate::Error::Deserialize)?); + + Ok(()) + } + + /// Load the store from the on-disk state, ignoring defaults + pub fn load_ignore_defaults(&mut self) -> crate::Result<()> { + let bytes = fs::read(&self.path)?; + self.cache = (self.deserialize_fn)(&bytes).map_err(crate::Error::Deserialize)?; + Ok(()) + } + + /// Inserts a key-value pair into the store. + pub fn set(&mut self, key: impl Into, value: impl Into) { + let key = key.into(); + let value = value.into(); + self.cache.insert(key.clone(), value.clone()); + let _ = self.emit_change_event(&key, Some(&value)); + } + + /// Returns a reference to the value corresponding to the key. + pub fn get(&self, key: impl AsRef) -> Option<&JsonValue> { + self.cache.get(key.as_ref()) + } + + /// Returns `true` if the given `key` exists in the store. + pub fn has(&self, key: impl AsRef) -> bool { + self.cache.contains_key(key.as_ref()) + } + + /// Removes a key-value pair from the store. + pub fn delete(&mut self, key: impl AsRef) -> bool { + let flag = self.cache.remove(key.as_ref()).is_some(); + if flag { + let _ = self.emit_change_event(key.as_ref(), None); + } + flag + } + + /// Clears the store, removing all key-value pairs. + /// + /// Note: To clear the storage and reset it to its `default` value, use [`reset`](Self::reset) instead. + pub fn clear(&mut self) { + let keys: Vec = self.cache.keys().cloned().collect(); + self.cache.clear(); + for key in &keys { + let _ = self.emit_change_event(key, None); + } + } + + /// Resets the store to its `default` value. + /// + /// If no default value has been set, this method behaves identical to [`clear`](Self::clear). + pub fn reset(&mut self) { + if let Some(defaults) = &self.defaults { + for (key, value) in &self.cache { + if defaults.get(key) != Some(value) { + let _ = self.emit_change_event(key, defaults.get(key)); + } + } + for (key, value) in defaults { + if !self.cache.contains_key(key) { + let _ = self.emit_change_event(key, Some(value)); + } + } + self.cache.clone_from(defaults); + } else { + self.clear() + } + } + + /// An iterator visiting all keys in arbitrary order. + pub fn keys(&self) -> impl Iterator { + self.cache.keys() + } + + /// An iterator visiting all values in arbitrary order. + pub fn values(&self) -> impl Iterator { + self.cache.values() + } + + /// An iterator visiting all key-value pairs in arbitrary order. + pub fn entries(&self) -> impl Iterator { + self.cache.iter() + } + + /// Returns the number of elements in the store. + pub fn len(&self) -> usize { + self.cache.len() + } + + /// Returns true if the store contains no elements. + pub fn is_empty(&self) -> bool { + self.cache.is_empty() + } + + fn emit_change_event(&self, key: &str, value: Option<&JsonValue>) -> crate::Result<()> { + let state = self.app.state::(); + let stores = state.stores.read().unwrap(); + let exists = value.is_some(); + self.app.emit( + "store://change", + ChangePayload { + path: &self.path, + resource_id: stores.get(&self.path).copied(), + key, + value, + exists, + }, + )?; + Ok(()) + } +} + +impl std::fmt::Debug for StoreInner { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("Store") + .field("path", &self.path) + .field("cache", &self.cache) + .finish() + } +} + +/// A key-value store, persisted to a file resolved with [`resolve_store_path`]. +/// +/// The values are kept in memory and written to disk on [`Store::save`], and also automatically +/// after each modification unless auto save has been disabled with +/// [`StoreBuilder::disable_auto_save`]. Any pending auto save is applied when the store is dropped. +/// +/// Create or load one with [`StoreExt::store`](crate::StoreExt::store) or [`StoreBuilder`]. +/// It is a [`Resource`], so it is also reachable from the frontend by its [`ResourceId`]; +/// closing that resource unregisters the store, meaning the next load creates a new instance. +pub struct Store { + auto_save: Option, + auto_save_debounce_sender: Arc>>>, + store: Arc>>, +} + +impl Resource for Store { + fn close(self: Arc) { + let store = self.store.lock().unwrap(); + let state = store.app.state::(); + let mut stores = state.stores.write().unwrap(); + stores.remove(&store.path); + } +} + +impl Store { + // /// Do something with the inner store, + // /// useful for batching some work if you need higher performance + // pub fn with_store(&self, f: impl FnOnce(&mut StoreInner) -> T) -> T { + // let mut store = self.store.lock().unwrap(); + // f(&mut store) + // } + + /// Inserts a key-value pair into the store. + pub fn set(&self, key: impl Into, value: impl Into) { + self.store.lock().unwrap().set(key.into(), value.into()); + let _ = self.trigger_auto_save(); + } + + /// Returns the value for the given `key` or `None` if the key does not exist. + pub fn get(&self, key: impl AsRef) -> Option { + self.store.lock().unwrap().get(key).cloned() + } + + /// Returns `true` if the given `key` exists in the store. + pub fn has(&self, key: impl AsRef) -> bool { + self.store.lock().unwrap().has(key) + } + + /// Removes a key-value pair from the store. + pub fn delete(&self, key: impl AsRef) -> bool { + let deleted = self.store.lock().unwrap().delete(key); + if deleted { + let _ = self.trigger_auto_save(); + } + deleted + } + + /// Clears the store, removing all key-value pairs. + /// + /// Note: To clear the storage and reset it to its `default` value, use [`reset`](Self::reset) instead. + pub fn clear(&self) { + self.store.lock().unwrap().clear(); + let _ = self.trigger_auto_save(); + } + + /// Resets the store to its `default` value. + /// + /// If no default value has been set, this method behaves identical to [`clear`](Self::clear). + pub fn reset(&self) { + self.store.lock().unwrap().reset(); + let _ = self.trigger_auto_save(); + } + + /// Returns a list of all keys in the store. + pub fn keys(&self) -> Vec { + self.store.lock().unwrap().keys().cloned().collect() + } + + /// Returns a list of all values in the store. + pub fn values(&self) -> Vec { + self.store.lock().unwrap().values().cloned().collect() + } + + /// Returns a list of all key-value pairs in the store. + pub fn entries(&self) -> Vec<(String, JsonValue)> { + self.store + .lock() + .unwrap() + .entries() + .map(|(k, v)| (k.to_owned(), v.to_owned())) + .collect() + } + + /// Returns the number of elements in the store. + pub fn length(&self) -> usize { + self.store.lock().unwrap().len() + } + + /// Returns true if the store contains no elements. + pub fn is_empty(&self) -> bool { + self.store.lock().unwrap().is_empty() + } + + /// Update the store from the on-disk state + /// + /// Note: + /// - This method loads the data and merges it with the current store, + /// this behavior will be changed to resetting to default first and then merging with the on-disk state in v3, + /// to fully match the store with the on-disk state, + /// use [`reload_ignore_defaults`](Self::reload_ignore_defaults) instead + /// - This method does not emit change events + pub fn reload(&self) -> crate::Result<()> { + self.store.lock().unwrap().load() + } + + /// Load the store from the on-disk state, ignoring defaults + /// + /// Note: This method does not emit change events + pub fn reload_ignore_defaults(&self) -> crate::Result<()> { + self.store.lock().unwrap().load_ignore_defaults() + } + + /// Saves the store to disk at the store's `path`. + pub fn save(&self) -> crate::Result<()> { + if let Some(sender) = self.auto_save_debounce_sender.lock().unwrap().take() { + let _ = sender.send(AutoSaveMessage::Cancel); + } + self.store.lock().unwrap().save() + } + + /// Removes the store from the resource table + pub fn close_resource(&self) { + let store = self.store.lock().unwrap(); + let app = store.app.clone(); + let state = app.state::(); + let stores = state.stores.read().unwrap(); + if let Some(rid) = stores.get(&store.path).copied() { + drop(store); + drop(stores); + let _ = app.resources_table().close(rid); + } + } + + fn trigger_auto_save(&self) -> crate::Result<()> { + let Some(auto_save_delay) = self.auto_save else { + return Ok(()); + }; + if auto_save_delay.is_zero() { + return self.save(); + } + let mut auto_save_debounce_sender = self.auto_save_debounce_sender.lock().unwrap(); + if let Some(ref sender) = *auto_save_debounce_sender { + let _ = sender.send(AutoSaveMessage::Reset); + return Ok(()); + } + let (sender, mut receiver) = unbounded_channel(); + auto_save_debounce_sender.replace(sender); + drop(auto_save_debounce_sender); + let store = self.store.clone(); + let auto_save_debounce_sender = self.auto_save_debounce_sender.clone(); + tauri::async_runtime::spawn(async move { + loop { + select! { + should_cancel = receiver.recv() => { + if matches!(should_cancel, Some(AutoSaveMessage::Cancel) | None) { + return; + } + } + _ = sleep(auto_save_delay) => { + auto_save_debounce_sender.lock().unwrap().take(); + let _ = store.lock().unwrap().save(); + return; + } + }; + } + }); + Ok(()) + } + + fn apply_pending_auto_save(&self) { + // Cancel and save if auto save is pending + let auto_save_debounce_sender = self.auto_save_debounce_sender.lock().unwrap().take(); + if let Some(sender) = auto_save_debounce_sender { + let _ = sender.send(AutoSaveMessage::Cancel); + let _ = self.save(); + }; + } +} + +impl Drop for Store { + fn drop(&mut self) { + self.apply_pending_auto_save(); + } +} diff --git a/plugins/store/tsconfig.json b/plugins/store/tsconfig.json new file mode 100644 index 0000000..4374468 --- /dev/null +++ b/plugins/store/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "rootDir": "guest-js" + }, + "include": ["guest-js/*.ts"] +} diff --git a/plugins/stronghold/CHANGELOG.md b/plugins/stronghold/CHANGELOG.md new file mode 100644 index 0000000..600f2eb --- /dev/null +++ b/plugins/stronghold/CHANGELOG.md @@ -0,0 +1,111 @@ +# Changelog + +## [2.3.2] + +- [`f8053e65`](https://github.com/tauri-apps/plugins-workspace/commit/f8053e659e4ccd85c1f52833411ff8417cbc5e69) ([#3527](https://github.com/tauri-apps/plugins-workspace/pull/3527) by [@Legend-Master](https://github.com/tauri-apps/plugins-workspace/../../Legend-Master)) Documented Cargo feature flags in each plugin's crate-level documentation. + +## \[2.3.1] + +- [`93426f85`](https://github.com/tauri-apps/plugins-workspace/commit/93426f85120f49beb9f40222bff45185a32d54a9) Fixed an issue that caused docs.rs builds to fail. No user facing changes. + +## \[2.3.0] + +- [`f209b2f2`](https://github.com/tauri-apps/plugins-workspace/commit/f209b2f23cb29133c97ad5961fb46ef794dbe063) ([#2804](https://github.com/tauri-apps/plugins-workspace/pull/2804) by [@renovate](https://github.com/tauri-apps/plugins-workspace/../../renovate)) Updated tauri to 2.6 + +## \[2.2.1] + +- [`f634e524`](https://github.com/tauri-apps/plugins-workspace/commit/f634e5248ebe428f8305a59f74c13fc15147fb8e) This is an "empty" release to update the plugins' source files on crates.io and docs.rs. This should fix docs.rs build failures for projects using tauri plugins as dependencies. + +## \[2.2.0] + +- [`3a79266b`](https://github.com/tauri-apps/plugins-workspace/commit/3a79266b8cf96a55b1ae6339d725567d45a44b1d) ([#2173](https://github.com/tauri-apps/plugins-workspace/pull/2173) by [@FabianLars](https://github.com/tauri-apps/plugins-workspace/../../FabianLars)) Bumped all plugins to `v2.2.0`. From now, the versions for the Rust and JavaScript packages of each plugin will be in sync with each other. + +## \[2.0.1] + +- [`a1a82208`](https://github.com/tauri-apps/plugins-workspace/commit/a1a82208ed4ab87f83310be0dc95428aec9ab241) ([#1873](https://github.com/tauri-apps/plugins-workspace/pull/1873) by [@lucasfernog](https://github.com/tauri-apps/plugins-workspace/../../lucasfernog)) Downgrade MSRV to 1.77.2 to support Windows 7. + +## \[2.0.0] + +- [`e2c4dfb6`](https://github.com/tauri-apps/plugins-workspace/commit/e2c4dfb6af43e5dd8d9ceba232c315f5febd55c1) Update to tauri v2 stable release. + +## \[2.0.0-rc.1] + +- [`e2e97db5`](https://github.com/tauri-apps/plugins-workspace/commit/e2e97db51983267f5be84d4f6f0278d58834d1f5) ([#1701](https://github.com/tauri-apps/plugins-workspace/pull/1701) by [@lucasfernog](https://github.com/tauri-apps/plugins-workspace/../../lucasfernog)) Update to tauri 2.0.0-rc.8 + +## \[2.0.0-rc.0] + +- [`9887d1`](https://github.com/tauri-apps/plugins-workspace/commit/9887d14bd0e971c4c0f5c1188fc4005d3fc2e29e) Update to tauri RC. + +## \[2.0.0-beta.8] + +- [`99d6ac0f`](https://github.com/tauri-apps/plugins-workspace/commit/99d6ac0f9506a6a4a1aa59c728157190a7441af6) ([#1606](https://github.com/tauri-apps/plugins-workspace/pull/1606) by [@FabianLars](https://github.com/tauri-apps/plugins-workspace/../../FabianLars)) The JS packages now specify the *minimum* `@tauri-apps/api` version instead of a single exact version. +- [`6de87966`](https://github.com/tauri-apps/plugins-workspace/commit/6de87966ecc00ad9d91c25be452f1f46bd2b7e1f) ([#1597](https://github.com/tauri-apps/plugins-workspace/pull/1597) by [@Legend-Master](https://github.com/tauri-apps/plugins-workspace/../../Legend-Master)) Update to tauri beta.25. + +## \[2.0.0-beta.7] + +- [`22a17980`](https://github.com/tauri-apps/plugins-workspace/commit/22a17980ff4f6f8c40adb1b8f4ffc6dae2fe7e30) ([#1537](https://github.com/tauri-apps/plugins-workspace/pull/1537) by [@lucasfernog](https://github.com/tauri-apps/plugins-workspace/../../lucasfernog)) Update to tauri beta.24. + +## \[2.0.0-beta.6] + +- [`76daee7a`](https://github.com/tauri-apps/plugins-workspace/commit/76daee7aafece34de3092c86e531cf9eb1138989) ([#1512](https://github.com/tauri-apps/plugins-workspace/pull/1512) by [@renovate](https://github.com/tauri-apps/plugins-workspace/../../renovate)) Update to tauri beta.23. + +## \[2.0.0-beta.5] + +- [`9013854f`](https://github.com/tauri-apps/plugins-workspace/commit/9013854f42a49a230b9dbb9d02774765528a923f)([#1382](https://github.com/tauri-apps/plugins-workspace/pull/1382)) Update to tauri beta.22. + +## \[2.0.0-beta.4] + +- [`430bd6f4`](https://github.com/tauri-apps/plugins-workspace/commit/430bd6f4f379bee5d232ae6b098ae131db7f178a)([#1363](https://github.com/tauri-apps/plugins-workspace/pull/1363)) Update to tauri beta.20. + +## \[2.0.0-beta.3] + +- [`bd1ed590`](https://github.com/tauri-apps/plugins-workspace/commit/bd1ed5903ffcce5500310dac1e59e8c67674ef1e)([#1237](https://github.com/tauri-apps/plugins-workspace/pull/1237)) Update to tauri beta.17. + +## \[2.0.0-beta.3] + +- [`a04ea2f`](https://github.com/tauri-apps/plugins-workspace/commit/a04ea2f38294d5a3987578283badc8eec87a7752)([#1071](https://github.com/tauri-apps/plugins-workspace/pull/1071)) The global API script is now only added to the binary when the `withGlobalTauri` config is true. + +## \[2.0.0-beta.2] + +- [`99bea25`](https://github.com/tauri-apps/plugins-workspace/commit/99bea2559c2c0648c2519c50a18cd124dacef57b)([#1005](https://github.com/tauri-apps/plugins-workspace/pull/1005)) Update to tauri beta.8. + +## \[2.0.0-beta.1] + +- [`569defb`](https://github.com/tauri-apps/plugins-workspace/commit/569defbe9492e38938554bb7bdc1be9151456d21) Update to tauri beta.4. + +## \[2.0.0-beta.0] + +- [`d198c01`](https://github.com/tauri-apps/plugins-workspace/commit/d198c014863ee260cb0de88a14b7fc4356ef7474)([#862](https://github.com/tauri-apps/plugins-workspace/pull/862)) Update to tauri beta. + +## \[2.0.0-alpha.6] + +- [`387c2f9`](https://github.com/tauri-apps/plugins-workspace/commit/387c2f9e0ce4c75c07ffa3fd76391a25b58f5daf)([#802](https://github.com/tauri-apps/plugins-workspace/pull/802)) Update to @tauri-apps/api v2.0.0-alpha.13. + +## \[2.0.0-alpha.5] + +- [`387c2f9`](https://github.com/tauri-apps/plugins-workspace/commit/387c2f9e0ce4c75c07ffa3fd76391a25b58f5daf)([#802](https://github.com/tauri-apps/plugins-workspace/pull/802)) Update to @tauri-apps/api v2.0.0-alpha.12. + +## \[2.0.0-alpha.4] + +- [`e438e0a`](https://github.com/tauri-apps/plugins-workspace/commit/e438e0a62d4b430a5159f05f13ecd397dd891a0d)([#676](https://github.com/tauri-apps/plugins-workspace/pull/676)) Update to @tauri-apps/api v2.0.0-alpha.11. + +## \[2.0.0-alpha.3] + +- [`5c13736`](https://github.com/tauri-apps/plugins-workspace/commit/5c137365c60790e8d4037d449e8237aa3fffdab0)([#673](https://github.com/tauri-apps/plugins-workspace/pull/673)) Update to @tauri-apps/api v2.0.0-alpha.9. + +## \[2.0.0-alpha.2] + +- [`4e2cef9`](https://github.com/tauri-apps/plugins-workspace/commit/4e2cef9b702bbbb9cf4ee17de50791cb21f1b2a4)([#593](https://github.com/tauri-apps/plugins-workspace/pull/593)) Update to alpha.12. + +## \[2.0.0-alpha.2] + +- [`aba07c2`](https://github.com/tauri-apps/plugins-workspace/commit/aba07c27b887c1cc54026024227cb3f74c91e21a)([#468](https://github.com/tauri-apps/plugins-workspace/pull/468)) Change the argument name of the `Stronghold.remove` from `location` to `recordPath` to match the Stronghold command argument +- [`d74fc0a`](https://github.com/tauri-apps/plugins-workspace/commit/d74fc0a097996e90a37be8f57d50b7d1f6ca616f)([#555](https://github.com/tauri-apps/plugins-workspace/pull/555)) Update to alpha.11. + +## \[2.0.0-alpha.1] + +- [`b3d7d11`](https://github.com/tauri-apps/plugins-workspace/commit/b3d7d11ae32e0970184f53be26e2c5530f78cdf0)([#416](https://github.com/tauri-apps/plugins-workspace/pull/416)) Added `Stronghold.load` and removed its constructor. + +## \[2.0.0-alpha.0] + +- [`717ae67`](https://github.com/tauri-apps/plugins-workspace/commit/717ae670978feb4492fac1f295998b93f2b9347f)([#371](https://github.com/tauri-apps/plugins-workspace/pull/371)) First v2 alpha release! diff --git a/plugins/stronghold/Cargo.toml b/plugins/stronghold/Cargo.toml new file mode 100644 index 0000000..e6038a0 --- /dev/null +++ b/plugins/stronghold/Cargo.toml @@ -0,0 +1,49 @@ +[package] +name = "tauri-plugin-stronghold" +version = "2.3.2" +description = "Store secrets and keys using the IOTA Stronghold secret management engine." +authors = { workspace = true } +license = { workspace = true } +edition = { workspace = true } +rust-version = { workspace = true } +repository = { workspace = true } +links = "tauri-plugin-stronghold" +exclude = [ + "/banner.png", + "/guest-js", + "/package.json", + "/rollup.config.js", + "/tsconfig.json", +] + +[package.metadata.platforms.support] +windows = { level = "full", notes = "" } +linux = { level = "full", notes = "" } +macos = { level = "full", notes = "" } +android = { level = "full", notes = "" } +ios = { level = "full", notes = "" } + +[build-dependencies] +tauri-plugin = { workspace = true, features = ["build"] } + +[dependencies] +serde = { workspace = true } +serde_json = { workspace = true } +tauri = { workspace = true } +log = { workspace = true } +thiserror = { workspace = true } +iota_stronghold = "2" +iota-crypto = "0.23" +hex = "0.4" +zeroize = { version = "1", features = ["zeroize_derive"] } +rust-argon2 = { version = "2", optional = true } +rand_chacha = { version = "0.9", optional = true } +rand_core = { version = "0.9", features = ["os_rng"], optional = true } + +[dev-dependencies] +rand = "0.9" +rusty-fork = "0.3" + +[features] +default = ["kdf"] +kdf = ["dep:rust-argon2", "dep:rand_chacha", "dep:rand_core"] diff --git a/plugins/stronghold/LICENSE.spdx b/plugins/stronghold/LICENSE.spdx new file mode 100644 index 0000000..cdd0df5 --- /dev/null +++ b/plugins/stronghold/LICENSE.spdx @@ -0,0 +1,20 @@ +SPDXVersion: SPDX-2.1 +DataLicense: CC0-1.0 +PackageName: tauri +DataFormat: SPDXRef-1 +PackageSupplier: Organization: The Tauri Programme in the Commons Conservancy +PackageHomePage: https://tauri.app +PackageLicenseDeclared: Apache-2.0 +PackageLicenseDeclared: MIT +PackageCopyrightText: 2019-2022, The Tauri Programme in the Commons Conservancy +PackageSummary: Tauri is a rust project that enables developers to make secure +and small desktop applications using a web frontend. + +PackageComment: The package includes the following libraries; see +Relationship information. + +Created: 2019-05-20T09:00:00Z +PackageDownloadLocation: git://github.com/tauri-apps/tauri +PackageDownloadLocation: git+https://github.com/tauri-apps/tauri.git +PackageDownloadLocation: git+ssh://github.com/tauri-apps/tauri.git +Creator: Person: Daniel Thompson-Yvetot \ No newline at end of file diff --git a/plugins/stronghold/LICENSE_APACHE-2.0 b/plugins/stronghold/LICENSE_APACHE-2.0 new file mode 100644 index 0000000..4947287 --- /dev/null +++ b/plugins/stronghold/LICENSE_APACHE-2.0 @@ -0,0 +1,177 @@ + + 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 \ No newline at end of file diff --git a/plugins/stronghold/LICENSE_MIT b/plugins/stronghold/LICENSE_MIT new file mode 100644 index 0000000..4d75472 --- /dev/null +++ b/plugins/stronghold/LICENSE_MIT @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2017 - Present Tauri Apps 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. \ No newline at end of file diff --git a/plugins/stronghold/README.md b/plugins/stronghold/README.md new file mode 100644 index 0000000..eaa9d53 --- /dev/null +++ b/plugins/stronghold/README.md @@ -0,0 +1,169 @@ +![plugin-stronghold](https://github.com/tauri-apps/plugins-workspace/raw/v2/plugins/stronghold/banner.png) + +Store secrets and keys using the [IOTA Stronghold](https://github.com/iotaledger/stronghold.rs) secret management engine. + +| Platform | Supported | +| -------- | --------- | +| Linux | ✓ | +| Windows | ✓ | +| macOS | ✓ | +| Android | ✓ | +| iOS | ✓ | + +## Install + +_This plugin requires a Rust version of at least **1.77.2**_ + +There are three general methods of installation that we can recommend. + +1. Use crates.io and npm (easiest, and requires you to trust that our publishing pipeline worked) +2. Pull sources directly from Github using git tags / revision hashes (most secure) +3. Git submodule install this repo in your tauri project and then use file protocol to ingest the source (most secure, but inconvenient to use) + +Install the Core plugin by adding the following to your `Cargo.toml` file: + +`src-tauri/Cargo.toml` + +```toml +[dependencies] +tauri-plugin-stronghold = "2.0.0" +# alternatively with Git: +tauri-plugin-stronghold = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v2" } +``` + +Due to an [upstream bug](https://github.com/tauri-apps/plugins-workspace/issues/2048) we also recommend that you add this to your `Cargo.toml` file: + +```toml +[profile.dev.package.scrypt] +opt-level = 3 +``` + +You can install the JavaScript Guest bindings using your preferred JavaScript package manager: + +> Note: If your JavaScript package manager cannot install packages from git monorepos, you can still use the code by manually copying the [Guest bindings](./guest-js/index.ts) into your source files. + +```sh +pnpm add @tauri-apps/plugin-stronghold +# or +npm add @tauri-apps/plugin-stronghold +# or +yarn add @tauri-apps/plugin-stronghold +``` + +## Usage + +First you need to register the core plugin with Tauri: + +`src-tauri/src/lib.rs` + +```rust +fn main() { + tauri::Builder::default() + .plugin(tauri_plugin_stronghold::Builder::new(|password| { + // Hash the password here with e.g. argon2, blake2b or any other secure algorithm + // Here is an example implementation using the `rust-argon2` crate for hashing the password + + use argon2::{hash_raw, Config, Variant, Version}; + + let config = Config { + lanes: 4, + mem_cost: 10_000, + time_cost: 10, + variant: Variant::Argon2id, + version: Version::Version13, + ..Default::default() + }; + + let salt = "your-salt".as_bytes(); + + let key = hash_raw(password.as_ref(), salt, &config).expect("failed to hash password"); + + key.to_vec() + }) + .build()) + .run(tauri::generate_context!()) + .expect("error while running tauri application"); +} +``` + +Afterwards all the plugin's APIs are available through the JavaScript guest bindings: + +```javascript +import { Stronghold, Location, Client } from "tauri-plugin-stronghold-api"; +import { appDataDir } from "@tauri-apps/api/path"; + +const initStronghold = async () => { + const vaultPath = `${await appDataDir()}/vault.hold`; + + const vaultKey = "The key to the vault"; + + const stronghold = await Stronghold.load(vaultPath, vaultKey); + + let client: Client; + + const clientName = "name your client"; + + try { + client = await hold.loadClient(clientName); + } catch { + client = await hold.createClient(clientName); + } + + return { + stronghold, + client, + }; +}; + +const { stronghold, client } = await initStronghold(); + +const store = client.getStore(); + +const key = "my_key"; + +// Insert a record to the store + +const data = Array.from(new TextEncoder().encode("Hello, World!")); + +await store.insert(key, data); + +// Read a record from store + +const data = await store.get(key); + +const value = new TextDecoder().decode(new Uint8Array(data)); + +// Save your updates + +await stronghold.save(); + +// Remove a record from store + +await store.remove(key); +``` + +## Contributing + +PRs accepted. Please make sure to read the Contributing Guide before making a pull request. + +## Partners + + + + + + + +
+ + CrabNebula + +
+ +For the complete list of sponsors please visit our [website](https://tauri.app#sponsors) and [Open Collective](https://opencollective.com/tauri). + +## License + +Code: (c) 2015 - Present - The Tauri Programme within The Commons Conservancy. + +MIT or MIT/Apache 2.0 where applicable. diff --git a/plugins/stronghold/SECURITY.md b/plugins/stronghold/SECURITY.md new file mode 100644 index 0000000..4f09bba --- /dev/null +++ b/plugins/stronghold/SECURITY.md @@ -0,0 +1,23 @@ +# Security Policy + +**Do not report security vulnerabilities through public GitHub issues.** + +**Please use the [Private Vulnerability Disclosure](https://docs.github.com/en/code-security/security-advisories/guidance-on-reporting-and-writing-information-about-vulnerabilities/privately-reporting-a-security-vulnerability#privately-reporting-a-security-vulnerability) feature of GitHub.** + +Include as much of the following information: + +- Type of issue (e.g. improper input parsing, privilege escalation, etc.) +- The location of the affected source code (tag/branch/commit or direct URL) +- Any special configuration required to reproduce the issue +- The distribution affected or used to help us with reproduction of the issue +- Step-by-step instructions to reproduce the issue +- Ideally a reproduction repository +- Impact of the issue, including how an attacker might exploit the issue + +We prefer to receive reports in English. + +## Contact + +Please disclose a vulnerability or security relevant issue here: [https://github.com/tauri-apps/plugins-workspace/security/advisories/new](https://github.com/tauri-apps/plugins-workspace/security/advisories/new). + +Alternatively, you can also contact us by email via [security@tauri.app](mailto:security@tauri.app). diff --git a/plugins/stronghold/api-iife.js b/plugins/stronghold/api-iife.js new file mode 100644 index 0000000..6fabb2a --- /dev/null +++ b/plugins/stronghold/api-iife.js @@ -0,0 +1 @@ +if("__TAURI__"in window){var __TAURI_PLUGIN_STRONGHOLD__=function(t,e){"use strict";class r{constructor(t,e){this.type=t,this.payload=e}static generic(t,e){return new r("Generic",{vault:t,record:e})}static counter(t,e){return new r("Counter",{vault:t,counter:e})}}class n{constructor(t){this.procedureArgs=t}async generateSLIP10Seed(t,r){return await e.invoke("plugin:stronghold|execute_procedure",{...this.procedureArgs,procedure:{type:"SLIP10Generate",payload:{output:t,sizeBytes:r}}}).then(t=>Uint8Array.from(t))}async deriveSLIP10(t,r,n,a){return await e.invoke("plugin:stronghold|execute_procedure",{...this.procedureArgs,procedure:{type:"SLIP10Derive",payload:{chain:t,input:{type:r,payload:n},output:a}}}).then(t=>Uint8Array.from(t))}async recoverBIP39(t,r,n){return await e.invoke("plugin:stronghold|execute_procedure",{...this.procedureArgs,procedure:{type:"BIP39Recover",payload:{mnemonic:t,passphrase:n,output:r}}}).then(t=>Uint8Array.from(t))}async generateBIP39(t,r){return await e.invoke("plugin:stronghold|execute_procedure",{...this.procedureArgs,procedure:{type:"BIP39Generate",payload:{output:t,passphrase:r}}}).then(t=>Uint8Array.from(t))}async getEd25519PublicKey(t){return await e.invoke("plugin:stronghold|execute_procedure",{...this.procedureArgs,procedure:{type:"PublicKey",payload:{type:"Ed25519",privateKey:t}}}).then(t=>Uint8Array.from(t))}async signEd25519(t,r){return await e.invoke("plugin:stronghold|execute_procedure",{...this.procedureArgs,procedure:{type:"Ed25519Sign",payload:{privateKey:t,msg:r}}}).then(t=>Uint8Array.from(t))}}class a{constructor(t,e){this.path=t,this.name=e}getVault(t){return new s(this.path,this.name,t)}getStore(){return new o(this.path,this.name)}}class o{constructor(t,e){this.path=t,this.client=e}async get(t){return await e.invoke("plugin:stronghold|get_store_record",{snapshotPath:this.path,client:this.client,key:t}).then(t=>t&&Uint8Array.from(t))}async insert(t,r,n){await e.invoke("plugin:stronghold|save_store_record",{snapshotPath:this.path,client:this.client,key:t,value:r,lifetime:n})}async remove(t){return await e.invoke("plugin:stronghold|remove_store_record",{snapshotPath:this.path,client:this.client,key:t}).then(t=>t&&Uint8Array.from(t))}}class s extends n{constructor(t,e,r){super({snapshotPath:t,client:e,vault:r}),this.path=t,this.client=e,this.name=r}async insert(t,r){await e.invoke("plugin:stronghold|save_secret",{snapshotPath:this.path,client:this.client,vault:this.name,recordPath:t,secret:r})}async remove(t){await e.invoke("plugin:stronghold|remove_secret",{snapshotPath:this.path,client:this.client,vault:this.name,recordPath:t.payload.record})}}class i{constructor(t){this.path=t}static async load(t,r){return await e.invoke("plugin:stronghold|initialize",{snapshotPath:t,password:r}).then(()=>new i(t))}async unload(){await e.invoke("plugin:stronghold|destroy",{snapshotPath:this.path})}async loadClient(t){return await e.invoke("plugin:stronghold|load_client",{snapshotPath:this.path,client:t}).then(()=>new a(this.path,t))}async createClient(t){return await e.invoke("plugin:stronghold|create_client",{snapshotPath:this.path,client:t}).then(()=>new a(this.path,t))}async save(){await e.invoke("plugin:stronghold|save",{snapshotPath:this.path})}}return t.Client=a,t.Location=r,t.Store=o,t.Stronghold=i,t.Vault=s,t}({},window.__TAURI__.core);Object.defineProperty(window.__TAURI__,"stronghold",{value:__TAURI_PLUGIN_STRONGHOLD__})} diff --git a/plugins/stronghold/banner.png b/plugins/stronghold/banner.png new file mode 100644 index 0000000000000000000000000000000000000000..9a13cf72e424ade2d38266082205d617cc46aa8e GIT binary patch literal 43820 zcmX_H1yqyY`+r9|DZhXMQU(mrQA4GqMTrrD5>i7Lij=?r=@M+3(J+)!q;o@BsZjzl z5fGJbC8hs&{?7Sxj)&uTci(%T{5+rMxz7ysZ?GIceH?-y7HzF-Mi6w23_-BcV+`Pv z`gaeTz%TeiEptx@V&b6wgF&h3+~7l)r_l{{sJNGJ9{h*yo|>K-1eHZI?c35r(7R^s zYihS1!IlmJpH9yP3I+xH{P!ZctIl7NTy8?KIN&O^f1o3NZnX07Bz#mXul{_DbNoO@ z)wOSxT57L-L$qRU%EcJ9b3{_b#L}(ddj-e?A~v`=Fi>lz3=s=uLpimUS0I$e{g;2@1MyU%Y)xP z1N=7K$%z+D4|}sUo*=T0mikh&0$HUh(%c$|@*Yzi!<5;+EVnCHuAGJ<7u$Hp zQ`1~YZ{-92{`NXLTJUo4oFB;TJfB4?#cbMs5)s|;=U|NJ8MO}4dbhI3qT2$yE+0iO zai#iv_sxe9(ooi(yBqVJvqb&i$yc(c!3WiT3qx-^XOkptT)zjb_e4hgCGSqo%*@zW z*W(X@i-UHDzqB89wHF?m${5LBI`8iF`$Bdyn~f6BVXMXy*6DSNVprz&#YWG~KHiXU zIrm>5FRP^#ig2s(guuvpW1rvL5DAwY_1zx#5t$7Opu;?359{_lqd9nW%{OWfVm2<` zTYUIoF(AZMx1&t!5C2@TU+`r(Hrw^B3WBYSNc8Qok@H^qDkSH*c_e&ky>WfVjOJ1} z3wi&&Q@2X}uESAlp`gmmFlicTRSVRcOW~8644o_qw9E!#k@#P8m#plew)5>fI8M=R z*u2z?0EDU4kuB@^b5wG)Tfz&GbpP=0zWHurWhZVW;Srzz_u5W)7XAIhJ)n)l;2pW4 z-`#{3l&PDEvioMM3UT*3Lda8>wp56RuY?gjpx1UrzNvJnYRJeTaprH9nkTIkGhZagXNVbtDpIcWSI4`MFb&*Sy8rx`dhI7nRk-{}i->$nNU-VGGe zbH9Fz#aHG~dz|$cX2hc`5NN-sM8?qLaNk$NBP5gDW9|@@7vJrDmPc0{W~l2EI+$-@ zpxo!H;lmSR+v#4vqE9GfR#h2lcRhP!?#j<<>$l-DM~e%y@SgqlyeEDq@!DE552SxJ z7fSAH^!OJALna8373QZ&9FLyorl>8v<|Q<;$92B^d=*(m*Yw^asNaeKQ@c=%;@ST1 zDcxLyf+)xH)qWQ*v$k5Nl7L5^p6f)r_9&&T3Rjuv#?4`i;E|1Ml0YYnDXzrbvy#6h z1r=}wu(>c?!drvy=6>3S41_EPc&GhVftowy{ko31Nea6p!Y{&Pg@tz2l8yt>1hZ6C zTYp=);_3DBKxIq#(%^h4nm^7FQkeSqjDu1&p_dPXc7-Flku?eJmE9OoD|=2w*jF;n zCnen?T?N{e9B+Bf>FN;-scxHx!FTNxkZ&r`jhkYZ20Ke%PUAx)$Y-?2gJjisDxUva zhp$gptpcA`rkf<8C*ax*CEqw+;AMu`hBM5-b}vSxDVVwKGZIu5N?-P(z{&I7fJ@xU zPMIL?3LNE2L^`m_@_TN6($ShZ3f}y*mvP;YVJ^9HyyzZh?qo{Oh(s!N}4}G zTME&>;W^u@r+mB%k}7$OU3iENT!mDR<}T3o+u&`(4hyzT?aEu@$J&8|2iN(z9FXoq4!KDh#aROl3R^F%KY5<@b3%_Ftf*W66C<} z6%=%k3+$lLJQBY9hL!x)hS4dGULLq=_!CzC-6QW@GFn7|%%|kHQC29~d*n=q4TfZF)T?0YVw63)(Q{Pk?7&38Rm( z*QZ;}v7$RMd9)_q%&*Jx5G3_86uf#mclUSJ^}j;=_!u-p&7z%8Yz*2VV&*(lsRnE{ zXz(@zVFKAqcVL8g_r!nJy<-5~9>u>mT_|`|a@#w_-27h3n;X%}1!>Ivj3WB!wTNdy ze|~9lf%Ov7-qG3jW-2L)8R|1)7xYnKD5%nrREMrlzX*-z|2sBN{3m6_JC5(yR7$M< zRfgVo3@DN<53tcTM33n*5E+Y>k{FV2Gu((0P$F%5Ij~!iaBpD*MPU173ekS2)MG=J zuhQ8=W_e6p5@JQmN~zNHOA8Ox8f&$cb3yNQ z!lZq3=1$As%dO*q#`C}nj;M;WBPrGfdZ!QE#{xggvaUa6vrvxKwTPTwklgxN*!Uy3 zZuc#Lx!+e_o>DX*BNvyF4dFB#{4(V|9Z)e!{0W_yEu!ZvqTNM}2QqofI+_H`0%y7+ zvtU=HZ44dQYjXj2bEX_B_&I)9n~1yUh_@HBP1&wr4LrKKsAqMsfoC90vL|TQZFq$B zcFN1BRt3?T)w9QG6WslVLt~gBD`OIw)2oICBIPUV&;F%K<_OI=NwNV(vEyQ5PD?S`*#v-e)MN+G!8-=*Bnk16&1Mz zW_Z_06u?YGG(YjB)mT)YJDb9nKYk6?k4@?H11b1rEV@BoF4)daV^$}GRXa9smkfTe#@uHMjQM*^jPRz9ZmVn!xfjeeYFHYloxELDCeBUDNH0H85Q>W0!Wb@r` zb2R;2#1gIcFyvWxTqYI_SozfVqe#`o;* z`q4H$!1{hTXQ%$pPs32ybd|@zQ+2k#v?!DHza8&JUP7K1rQNv6@OE+PK^V&61Ppn@ z{Q0@)-8Rw3_7HbmcTC;GpDjWKg7dHOPuBUG7KA?5R##VVRuO>d>q*YO)}_Z;jKeQ1 zRV?MbYU=*!pjND0#z+q~nqs|hURP4Pn5I&)IhilKFI`?{$BYrnZibeYu2?>K0>&Me zPFe7@2-_R9y$RvpjKy>E^76(DuR(Jqe(xt9o6RX(VUtbQL|6SM_6~h3&M@)B68((- z{46wBddon#5vTl~QFGvw5*zd_aU@N4B4FL?Y)Us@2av#QFHa5wrj-4dc*Qho{qnXj zgtS??vA{}~qDl`O*6ISfDQu;1+Z-nA;8m6PTQJ_h@zjqd+mu3sDSS^)&(Sw%LyJLU zfq-T&WYDB8uVn6b*KemzLFmo0Jfx8XYf{L`Dn$oc(-8s(Z9k1Y5$%UIY^)wpa%o5< z4P4&hZCB$AwDoi43A7Q|V&%|D)2t5Qq4VM_ss>Y2X?I=b9b!0s@2t=ALf>vi)%gxB zKRo}bg*~neAr9HygID}{hFfLo_@NVdk@Di z*XZ{e-&xil$Rk_a9yQKXA=p`HS>JFJTs+K|`C!Y|CIef3IQT_yUy(&MT1d#4?WsLR zfekt(Ld!u_XU-4Vz>4^gU%R^`0yDJ#ayl8pUu+-oGj|0DA@a?Nq3sf!RIZ`Y z142TyaO1#IYl8v^?x9D^3r}c^j_69!^7DBQE$mf=G%cW0=i5h^uNp|!B*dWMBB}fk zMCl?*lch711O3*#Xb$C*PLDB~eZZ}~TQyy1ItLxxljoXCn$I2l0Z~J`vd`w^(U)$= zm0kceFO;tg0(C~)|0bqCk~tCA%K-6NI$-mhOi7(+Mr?V)M^G8a^E^J401H`8oKAjD zT7*F_Y|Jy|KFb#03>4>7Q;|9+I=pzryr!7KL?yuF{6;thOX!zkVd0wAgORto!a~`L(?ztJmza&U6<$wjqQ1cPp@6TF6CA| z(4N5T4nzLg@tviaoAy#d8LZIP#<7kZJIY(*{9^cdhco9wKhj~5f-tD!A}|bi&J3)n zS$NEiy2HZg_2(=7Y@1WM6bGc1d6c7r%oA+!_gnjGU)Tg)A-{9pIu={=gBp#a>hk3Tfma-oE&8U&7%y*-|`Uf4xx9)e&qpgKfb) z*_{0vuWZQ9v{@=_hI28`!Y)%KILITwONJJDAzjaBtU%ObybVj%YinOD%jUCr#R6Uc zlICWm`-QoN`Ga90^eLoTN(za*#Dmv1O#z_cK%cgsgNIq0@o~$YA!uC}0J=e3e2TS) zSo;@TeCL`s)N~h%a2`J?Gdt;b=D(17tYtwaI@O;$g8_={G?M67@gj@_>GEEYl@&zy zVoMrjb?@N9>54nNfK7yE-WCL zsO6xO!UtGs1*gq|KlRJZ7D}DEpZRxfNfpeMKmLAkaq3Ga7oQi`9(4wY#Dlhx_;YCg)k~%?R<$1coV$$N z)e2`)%8+(uz-$`|@i)?fhOHdhP3r5s@Skrp%tfT%%4gJAfMKEHBn&z#V;*5#Q`kdL zr$MP$LlL<|SX;Kh;6Mh=ZZ>Pz{0NB8rO z$|mQ!04x_d9TG*xXbr!XKyk+vqo5@uEgTM<|DEwuU9IasPCz7KvUK8z_aMD3l&T%` z(9h8oiY$>JrESn3VBiS63vhwv3=-Ly7f;;v3Eo+FVgXhx9F0k8K2dZER-I%%CszE0 zwlj8e2BZp+c0(6Q7{T8oNFS3|JD-=93NY~%=u*6Y`tD&KD>sdlrTmtGWGfvmOE}pq zlnxu@TX$6EVuqMc{RiDT&$!1%q4eKFqZh~R^O&6G&v~wl5gqL8-oPNzMH%d(u%;1T z@n|pY%bRVH3;yXZS%@_-?kDqMbyPjY@D`R5i8}_>d+UqTI$KIPV6*iJZwRaIgAXgo zo4tywW>@Giyoz4&ooCb)I&k-1qd&0aL72>C=^r(Fy%7W1AnQJ);Or;9fVH$VVkOn>kqa^mDC4#9f61q+Wnf*E7$l*XaWs`%t(t1Alomm#u8`DB}*r3MsUd8y_ zYtYyFG*Do{0h87m>ltwTqvlFtUTZm|5$~T#Qcc}UfA#7WLx_7B9mhtzaPnM;6aFp< zfwY4o2xKEjUs}$C{q|q_M}5}g_Bk6cOfo8z&>)#AagLU`6DKOwu41PFO$s4t4{kld zzBI2mPDRleXaaEgKLL7vNowSVd5sRXc z(#K=Mg5HIrh;TWcI=&o?177MfQ53-SUzv&InfESkzL~CPw#Pi2`kbi9x3l| z7&MvKd_9y0qvgQ|5;I@Bj$s?pKE#~^9~x91aM}tmTf{Hd96a*f_y7K3o7JH0KWpwM zrYm)F9*jjr&syQ68q`2FX&eN*eBsAHis3uO{K^XFI(QwzK zb1jrf+w#ms&r8ggs<#K@qem8y>pvL?8Xf~^vpg-zcWzfTbstC_kiYaccX!63wf{ze$8UlMQoTDN%E1OaSv zMFHfaBUjq~|9CzYF={TD$3k{7tf=|o#N4C@JZE}U%chi|(j;c(8B#VbvC~U)r4W4$ zB3;;4Ggl}U;OG*9WkkY67vNZc0f6%zd4xPqrb_9X}o(H`^qU zW6cwI&fqH8hKRl5gQVhGlK5c;6^$I})0Up&NiH%-Ac;X5HG}24fY?rY#|YtH zbH~o7*sJrv>+@gQ@Qxy%;}kd2V><7kSZmCW($YO6BwgQzJbvBvVCn@&h8fk zRp=PzS+^LNG3;uZ@{sh+7|h-=$59QgoNsbK4^h4Rr=Ic3MAzo4VnUNOd%2?5!qDZ4r?OYB;J(sfY!=l0dqn_45kCuT@x^0o zNUmtb^Wy-9Y8Z1RJm;I`TZBTCyVhdGoRfM_>?H4z_T`(r*<*ljNIcJ}%bR3?_F~ty z`5T53X?BITqd^#2q%P9Y^neE`CUX)Nd8WPkF>EnKLcTXQ?Pb~{h|tHrD;}D{j6-mz zoBYKa!SY-vSQ$Wnf2Jw5*)+2#Ryz5Os(SRw#c_=RO#n~u=0H5}EPrPy2kxe(ZI9J9 z?#!f7b$=$OKf@`_gDr{husC=j)BrQaUO=V`V^0u^&jH$(drBwGH0j*0qM-4au?3}C zn3rVJ1^UWm)L~-rFyF{5`kX>BZ-?p~4;i{Vg=Ff%IQupPh&^(LUH{Md+cd<{0 z%XUv0dI3;Gx*Z`nF9|c?8&k}Vhus0yEk+B)ov!xNZ^oV`1kVRSZ5L=W=MnMR>^#SM z4s2RvNV-i@fEj2ipZZoV5I$(mu$vD8QPnvs1d3C5;`o;%AkUJiwn zkpry(ZrC(ZM%;HcM{q|i*%>gO7g!zOUA1|S58o=1kA3)zZ9z4))-5QaiO$zYE-o$}O1JU% zb!@G+;d4n#b;6LTnKwM7I&oTwiDQ@$jLLHryS%Fo+%(zp0SZfWd6jkUGnz-75+S?Ip8Iz_XX{|gr( zcmZAzhQei(GM7pXk|lxi`bETM?62T50{Y6j|1d)sZ|qL)*m*oQ#kFC_fpWXweH0+_ zzxp5!;h<7x(5qob_Zw^)JwTxQy@0UPshg_vW5C6x#HXmip{4*MjEc|7*ky{@PV&R@ zT=8rR?*+u0Rs=*&aOE7#$P7LFY#yonA0Ecqy{4-=f!(q{E~{y-uDZq#{lItxC&LB? ziq1vL-0Ph~(ejsB$)-HEV~@L;Lx)LOD3b^2LPhwo6+oXQgVeW&VhwXk_u2bJh>!Z# zpiTOKC_4v5S_rA%LaoYXE@xcSN#cQF&0b}YI_-n?4BHM)Kx7li<{V!7jlm7MIwu`! z6qJi$b{$@1y8}-0qB|XJn3t)R!uyBQxsrzwGJ9`ngMCKEy(_5e9s_hj4^aIZ4Kd`+ z=dmMf9LZu`%BctcL3kHxR6IYIPqMymL>G7lNxL+C*R}_-I_WSIh#Uy`X9Uu#3*fA=zZ75+T8OcAY`vR)|*s6Ep_7= zLq^P4Hs$p*_j?g{{zdx=QRC#Y$9zzeM*{09;uB=Xq0WCQ{E}-&=#L?vI_nKu9DRqj z)kgY&@H!yOmj&Tu_Fr_nZ>=LcFC+eKQ1^L{*Eb8YGl7kLtx)l<<7HN;RDt;9Do~zD zQVQoEW6{p@+sW|3IUI~Jexx2dUvX!vWa-|PG-T4lj+SB`Nz#>4q$Dd`qsF(UF^}@X z7d^yBUN9z{6{$19I&-^!_dJGe zt3kL3;bg!hF6!?|2cAXJcDwlP2mv;`_q3c3{cUCTTak6D z*fv4ES1JwF{5Dhc1nB`y|80QXZ3|jac+6`_kOPSJrR4$-%YbQMc|<|z-ni_Po7WLX zc+5*JY?l+GefRWE=$4)4TI!l7uuu*Jz_58MyxkqQY1BAQzd~uT94F)J~0?5oKNFC$S^tchpfMZryYAgvAA{1-d)pFm>gEUHOH7 zA`_yO%u&OaWIkZg6R(dqp3iER5 zB60PmGaZ2qgkK#~HA2?HqLCGLj>DD*gW!2A7BP$ZOVfXOv`SDi;v`r}T!ynRLs6DE zLsohK@0r?_%rK0g<0fUOqf&@zP03}aq1_zoFfocRVpcsERHmT~C-AVXTf-5O%nguL z0*&@@VNHgYeOW52zxW^wrfW@iFgpd8(MiGX@X&Kb-Ytni3)~r}?U$wMv7nL2sFLXmHw5#+zjqB4X*Vl>u}Rck#Ho3{e1iWOF}KS*NDkc{GL7Q1a$~>L_T# zs@HvUN6(#qw~)2V{_6KlvA}a+op;P~*YzI)hRCYI%FrP&(CQ`~gc0&S-je_bzs4~# zNjZlN^rBP|(SE)oC*oijQ80Z`txWmv#IB29@V|KFeT#vMDg1>h#eVoD<{N;~4AhJ?li>0P?|SAB!Bz{b5$ z_l*HYZPuiHqRQpWmCz}@Kt&o5qcRv2 zQa^QA{rEjc%-hvKnU1s@hf%IF$N_86(u(U0)Dv*{MI(ii7dbUeVw#iGmHO-hc6b@} z(AcK*^cgx5rjCG)GH3~aprv+w-AiA_9Z+8_NCCBy4gvs<1|%*QWGdNI46#FYw|uhu zKeO$MZ-d;h0rKC&SDT)9VHb(*|lf^gL^{~&MR0F zaaM$P3q{N86N~m@D-noh|eb5Cve6#ye0~5>(F?T_O6Qp7;*Ej{}AQkqr?q zref759y=_A>6_Y4j=pSOqYyU|OPQr&VHxi! z*7hgbKV2x*0RR(vukN5}UHi~>%@^L8@JoLsgN)LHtJxdY(%tLM;d@g4(WvE?BE9`< zXI>tgG8ah78~c{5UZV_JCd8YzJi9s|x^%oJ8pY^qU6(#>mJ7-y&=wuaJPC#5UQkQ6 ze1ZRgPEj*g=a^n(K9RI?7LmoQyQ16WZ^cEQ2k(HIw(|P2jxYAR5tWPESC$9=?NL7a zixrkVLAsRjz0S8}fJ6HFySuGr^dZPBkxsA+rAK@sVWqh+Jh@Y0bgWCs3*MJw{OZTG z#)Rp_E`Lidntuy_N1503e-YSqBNe_}#8CHHix4#)c4Rc82d!m@R6Hg-8@o8HAZJ`` zM>e9x5<{8KP}oShkL`4RIM%F4i_r-|!e?lqicYVay%iDe;KaLr@|3oI(-xGJ$L0^K zbd_K*yYO$ZU4+`R@>|9UKhVm{a*8s<(lp zvgSKUVXghaUS@e?OlpE~a#P0_u6Magmzv7qT@WNi=QW$`N6fQa#pebr5U$^B0nJDO z!j@g^5-7T4V=K7AiWl8g{D%MVr*-Kni z$%_RAL)D`%FgZ#O)qr2#GFPeBdOeC3!;An*=6y*?nl=;E^dsSiZpuxoFuqMA)lc5z z_qw(_>iu`Dg{P#ev7xBEn+}!u_6=sr_|)dRq-InH8?;>Dbb?gXES2Nwa2C2T0nV)j zwCoZ3u8_IZnQDkH&EX#sdS!Ai>cBgv)=7Kl1VcR~RerDg4!d?MJA0?{Iv;8JDs`cB zgipkmgRf183f6tsHR}Rr{0)TqCNeZy8 zkAH9<2U{nc9HqxfSbq=|rKLVan?OL*$V7xGFt^5@zfs+`Q({ScUBMhqT?sknt(H>r z-H+_dPJNWSzz?2>P9~$!z(TFSm;pCCRTSAM|0yc>OOIWnJ_SXJeA@84YUtz09p&eH zng7lIp0fQ%D^+5Vpl${~n6Fg17WEC@yuJ!ht5!wq0LUu!^&U0hrQnG%GHfX%NMk{%e)!*JtX-JPSp zGLSm9;YW%av_maX&%l)qxHZA}jxC5NHxHa7P*E>$}kZVvpo_YWI{?R#kHUaP=r?J*{yk zO@dhhG|ZP2+d;DB0fjJk%1@4yKd!06-uxK9nxg;0uV z(pBhMx^Xxz+wkWV!;dqpg319_hBLoDy^x6er+a(Af?7CcrD06S2vJ z$$_&otB2!_qkJFB?AlY04jPZPrzVrZl+;#a7Vr%UuK$rmvGaK)L7RmvO+lc4ss2BwO6KfeTp4VG+dcxt7B1>;2M!;~YH`gp z<(w7va82^4oJS@DiwSCv7R09w~0~t;rJ{l5>p?GofllcEG4m7c9!z|MznyJ zbBMsm;{IWL)_zW?M8{6<3eL$t*Py!2`gwT|$(p9E0Caf(LUqdR|Y>96V9kxBZ_1C2KOk776HK;rd5^?rLmL)svR1M^iyY z@@)esoW&% z2Lgc`pTGA#?RNSzWNe_vaGdQz#LyD~zx%=^+FLL3vx5BA{P}Q#omcjp8n>J>E?$`S z*_#qkkr&fJ{yO-kH}b?L;Lj)R*dnLVopUv#d^HW5MQ(aqvzl^qp5*n;#mw1?cd!D@ z`i8=*h?m+?zFCJG`DuzPwRUTn;=lN6;y>ieA5E7nZEqT9?G8;Th<-w*cDgTiie#lP z2AWNQyswl0Y^@HrbElkztk9T*S$eniv)(=};MX$|M0>@7YnGMV>ec<fv9i1sxg6Z4%Sh#yWxs~rAS-x@vO8{~2n-F{rxx#hoF zzpY<p#lt}-^ZKYqdgwg1sPz30U~{tyg_7peP^3@kF~ zBSoyR16GKoS!Mt*a9`aHhTSd)|9ukt75Tih`(X=;XQ=bXZNbFQ9p=|La}DpBUo$%# z&SxDuR%r1MY7e$X)0F*}t0<=w`s?3Og!XQ1#cOtY`-9|F9ME6F*{{4e(pQWtmDqD( z_*2K8Zoc8oYqQg3iqL!>erL#&yqNCt@MJ``WGeiUVytI&fZp;C1c-$ z>bGs&Eo}T2Fv&Y{|CFYggPd(j!ztNnV~1lB{xg;q;Q~J6UZ!p@uB6=F&R7k|H?O?? z0TH}u8vF%$N5MbmX{ECj_t3cB^F*;*?;liqT4=6oGB3;?==`+5xpsQOXCkl%Rpo9u zr&H#3tm}{MO{;-Zxyw9%b^<*GX8qPJSF7ENEhC)oiCtCRTdJs6tWM}xqx7y8t~~fJ z_zzcbS?QU!cgJhwgC)xMzPu~eJin?LRd2OWY)HOR*Y#VicreRA>1 z%(k2cOqw5^Ty=TQuJX5;4Wao{f&u%Qld+sl(&>o5@HSoEH#rHM*wE*D@;(kw@J*L> zuD9tFjun?l;^vO!Xtnf;%}$0fBu*(8s za$D2)@{%(bKNm~D_(mK|i3U|(f?tNWts-u_G!pf@{=9!N>K3JE&g%Yj`{t(ehB9x< z<$G{SlK`in$ws^3kPFsx-S7E2W7;D_GGi3+6?_==VNcvfwJWp zTPE)rv27uS)D%&*jyC)?1nq}32X(yGoTG@p}QCCBrAud1qw{Bof}P^Kez)TQQfU_#&(`r~_UiYAv8GhMZs zMaJ!SO@8-WDTF?vu!8eV_t5b4jmtubgy_;4xkSUef*L_s zU-MCM@|mID51^EHFIc1n)$Xb1cANmo%)KAbw@6Cy#-1q4p%2k@(W$^m zu_bjfZrS_TXvs1jTV5}H(F%*<12aPaP%{A(sZfMVkuDvNI)5YT5*XgTZ&t*Kh~!4{ zsM{753Iy-}iF6m0*?)2(hrAl6Pag$G8mGK%d>(jTYtJqA^_?)4z|AIFHqm1%XE-=L z#;;UT^63U5ENz8ryX)L- z-j6M1DtNuKtA8UZChS>TL`fnNOcyaPi^`>9CO~S`XQg;FI~ME|$C<$!o>8OdB!@ z_0GRGPy6Zx}aPXTX!D4*ZByHMB@9klU=0iWBm?8>ZGb0_LHQ6hLgHz`%Qv&u|e z4_sEVkuf2%@;l8{myC)B+c}TA?F6ni?5!~1vmbw^f{4gJT|#y9e>5=3Jz$!!596spq)&8x3e*GbclKY(XURqb~g z1PyFxtLyppwfQNbw)9U1j=TEy^(T==^`lJT_Nln0a{Jf1QL#3A-zcZs#K@9~#Y1A5 ziUfa&&MM*i#NAB>+d*sN2w(&59___Y!PxV%h zTq(QJKuKoIPWY#eMY*okB?CIQu(BU=ydQs{t9BucNI6r;a-N<=ff`%3PSQ4`;r^uc!PTI|j# z;`l42fEZ6h!KAFylbc#Wv(9@TYQ~Q{ZM_I_8N7P zf{fk!936B?eq!l{SdhftowbUrgQclj-}OI?C3ags z(?^pw{qXCC#XWn7ikVf?VzI;An!vPK9eU(?7I3ppZ_@amvwdQtbLD;VnkgYaci--r zR2?(`pBv4bFd;4@rVB=d#~oL~eSc6QHRh?tg|f(~o7kE2pI;NWXQTgD91JaM-@F_w5-dv}cD0dn zA1{9_<3M(_K&|Fe$C(+jWaEcfe}irNBPYdDyl24HW_|DGW?A;t8Z1ArgP55E!CE9_ z(d#V1uvdif_SO?D?nH-of{TA+iB0B2zRJP81JBIJlXag=-ooq2PyB?W6CKKjE|r^D^!Z2CD4smEB7TMEDq^dmt&bvMkXt1WutUvM|t5; z>fmHf;ty(Z*7^YX#_k$}uAtvscA(Fxjz)50A7qB#FgMBP33q*wIPwR8LLZIBN+n)- zev2&&I*Im3&hP=2cRgYLQHuWjY(}*tFcB!yms>4SNBTTay@YUd%*NX8^_3V zqn6LLeW&<7d&^yD?m1WyB+re-ZOg5$24&g#iYK{SeV?Uqwa!}7EnMjz%EMZYVikMj z?=>zge&(`%WGi;IWT`cbalGc`^7ff+WAb-z&#N^(TL$n-font)eGDRgsN4-XX(nbS z$4csIsx=FPgJgQ&oMB6bJEq0o!lVp`rcAeOfg2_kHnR*q!1--2<3CHE3paUjA;BR_ z@9SM2&8m(iU2sz4;RM0mrN7yZ>)h@!1hly36jH6<0aifAwzY62aq^ahZtb)WwdhFI z)QvHVaS0mnuAsYKU01CxR^bXF)1PX+k>SSQ5t7IpJF{Yra>1INJJ)TPYJK%Q`cOj$ zI36oj4o)p8r~k^AMYwlzB?BGBve);So5NS`v~ATD?7b~tRzBDmjF)n-&{4hH0aNi# zati13A+uv$ChC|@v1S#cq?7@*lqWw1sghBcZw{ZY$S$SaY@Lt;UB_W`j2xI06_RRw zP${o<`UP{>(3(=xe|I~7d`CI7ywVQSw;l{~EY#3BF8bmsW0I6rT0}~8;F2B7kdAbL z-X0O(?OxqeYzSYq54M;Z@Y}I=q3?N)m1HT?5~GngpIml4Ykx{4?TT827YDzYlkni4uKV{uC>pDuK*9!yfnFM}qB3 zfbDMs?yQuP=c>zk-a)wFKBm>5I+(3zh>%f}uxS`8Rd9*Kb~TwON`HB)|M!9ljLETAv%QZPTh; z=eVxq=AQFpY2<-i#4=B!&S zyPe#M1NeOA_s{LF2w>}S|6}p8?sfm$WA`yvr;zofk&2$L*i%}hE>0rxWP*A96~=L6 zEsM6(no`weSZ&ch80?bq zVx}IC&MqjD`c>Xi>f-&hYjI?|)B!KUr}Lw-XYq4&UDpu1!hO$)zw-vc-};qY7$}*6 zTO)Zc^1am~R(X9HAMP8Zoh<^um}m_IMU^EWgOJT1zmE4qP3+h!4|&6_>{iobqEPT| zPC(kGkNqh3bLjyOnvyPygUf>-sHHtye(qe^%g}|guBE@6))5(PZw44xQ!aHgO|9B_PiyM?if2bpojXqcJEm2i|q2d#lq>Spq zrAMyL!Yy@gikit-mZjnHqr6288r2s|CZA{6T|S6ZOb)_6E{<7^G=39p$nQ_1VPV!x z%P^ezY?2?oA4Ek16-c;o^+`NW3;PQ(RKxxn`66-UPEK{pD0$4}1zo9YIFVh^=vQOF}+&@Nsh{CjajOYiwEj()wa z3@ZEMWabbMX!O0zt`{++&OD@rbT4g9$DI{1IMhKhv`xbC`dqF1|QsyfFd(S^(s2xOH$&ji-tZbaXJC z-yXCpSK~ud4ycrEQHiH<+U4u>_$x0szS)-x24?ZA*h|)L7!6i`I4Y^%`X+SgOB)Kz zTv-wE&V^c)&!wC#D4ZG>zR#o=nh|2sm}{o21Y_fFz!hYzAZe*- z=9*YHi=)HW3sbMFGY?`@1YIA5`cBF1f8;Q@M6Jd&d~)WJo4Qeo4LQe5!(?uT$v9Fn z`?8$`g8u%@6JJM0OIOmw_O~u&2JOzzoEO}AN_qd^m~|vFOV0cF+CwRqj1pw)klP@~ zZz^rkK{s~S&aSnAP31h2mNCIxB6v+C7&SQ2gE1Q3=v7_vY1|uEd%t>M(YR?L)3@>e zy_TH_o4*mAvo8(O=%cEKZC6sM1_NqwaFdW}GuQA(1o!Ub2iKD~qdxv!z7Jf}AbcOCu<^Ln<%LAcm->^qnQc_7t zwiF^`FS3VM$(ATJ zIp;jveP8!=T{p)VlvC1Lm|>T$pDK<8D!Az|AYTxpI(I*-f%c&x?aQcQ26j+UP(0j- zlG@*R@9g)POd%do%tCDf{(k3@+}i6Q9`tlN?AA8!*T()~m_p@;gI%JwQVF!pkp|C*{LEbpsoF^q+2kC5w`4JQ_Q?^VZrS3UehMrrT!^Rki@&f_g2&AzrTaOEtakL9(vAv@F^e3t?iN}yRP1s z*KUiqXzD=WOMhW2{pf(__GUi7Kx4HftUeG)TnDcM`#`Da;QrQff z1)9k6hbd06_Pcc9(amFz0`~@czFIdKwuk*x+ZbK>yM|iG43()}QI2Gh^-M>RXA?Yc zeY!L{2~zuN{nsqLD%~1W9wlyCrCq)DqerRV7bFPwK}Iu%kHco^JNePyUjq9BS)jbQ zgmwGJsBJMJU@_sNbKw4X&xjuxYo6xEoXDEP;A3;GdNmlw;K+!|kdZ0{n9|6ej8yq6 zad?Y7X=2cCQN!GrJY|2H8N}F?%Co1pkzNN^X6iev;#I$+h6uK9!T}qtEH`nA8bm>{= zlC@(A=YD^|)wWsS^D?pbOoy=$05BXIbb){9VtL>1)$t4l@QTKwOBy!?8wdIV5&9Nk zf3UZ<<&O@wdZGb9fR(&$2(khyC$i{8VC3{}}Eg{b8Wqvb$`$mhTN-#P8Rc*e1>N@PByk#uDcR z&B^LpOZi2&Zk?Oh7B}^e)=5|`m9JJ@P|bPKmRi7|mK4&zz~l}{M~_Ti(H8IyDXrwunjiRMYG|j)j1Cn z!d^F^7hnCEdVNm2<*~nkjM3~J0fJ4)#f8&K%^PY{+ZA{-hsy%u5SquB%uRsw$jETRN!-xy&f<0`0zud9Lbj z$gd-adFFT{_VDtfmE9MWRWPO6l&ck%IF8Uxo>Q*d9?*#8E!~^6%u%!B!y;$=H5VKf zu>1r5HC+)J^eMtVqv?K)>_te8rmv@jVX_Zn`~WL?*}*Ct)FKbli=WvuG*y6VZ7$r0 zV@Cl6ARWGQi9e&yuqW+29%g#)5$7s)hB=9&abfft_y!4{6!IndH}IwEobQ^iq*P>x z@!ArLe=@L_@;~^i8zgOX`W`}HM|j-zolmk2RNWv-0g8*rpP-oSc%dzO?bhdfX2;`K zheGB=daP(ZH_KhO3$g^x{#Lk-VA(W_5SSBk5};1)jNE%3!yR2mr0-@xCPmfVU;G%W z{jk`Po@0qxTpP8{g6!uY!eETItilH`ZUk*~96~NgPX4LA;!QgsQd`Idh^paKhpMCe z@(l?Ez|mno-$Cu_fGDyK$t{e6CV zCfuxz6fbM5a7xEcOao*Xu@k8dA3|t=dE##r_dOML=?wNx+~joDrICAJ+p&wLOOZYwqa|7B^WtIPG=#ApC9z6XfT8WqlmhKjd~c>29_k2=bHdea6y~ptCS-K?oj7Hh zj!Ij7-cK+cT`&N@g^+fxAH+H?0 ziE&`Hauqf=)*qxmpF%4S*jCGmc$3=apIL`W+;rd}``oY7KgX+Pb z-|sG%#_Xu%I$KvfK(OlGUbn*2dkwd_Y0~QNi@ceFa7kT;Ug?SuTc(^&v5asDOc`NO z_`4F1C>p5{T%ziImi1FfsLYsQq(FN~VTwvEyBZ3db*rCY#n*PM>jVu77x<`dJoS!$ zY#US$5;H;YNo^Ijl58T>b@+gTPe+hvSc8hl%slOX=+4dA)%$3p>^CH(@G}C5xz{KF zh4d&4Oqit!;ja-etulN0>kX}!Zi@tpc{I0dW*9)2V$z%*mhXZ*5FHh;Zo&3uj}NS4 z103aogi&a#GrErGEKTPdLL>6${8#7e1Kx~%-QB^oyi>Ma`GG>piMJU(YQswFOVM9R zXFnq@EyeV49AE}P1TdGw{CD!rQ$6BP`>U17CFH;DijxP;TnE7!3?^+%9|V9xrv|W2 z*o@G(Uf&#@pA%AR+#Pdh5$AYn)4tr*ELR#~z=_EF$nZ7yR8otpLZQ7;;(;SaaiRf+ zLn(<|UJ*sgduf8UV!CVEFhFn5Dg9{H$74aipF?ndGB?fVYbM89=9SZUV}c$XLbL7l z#@8so+2s*E35}=zwdTa*Foo-AgG=hT7*YbfwcOM9pKXzGfepdT&1M1Q**OUD{o;ff zWGr!dmzX^Ajk~KjLhMxMQW6JFDO)sQN)s?HUA%DeBzXO@W)0yKRD_3PsX-u-t2JWxEmFXKT}*wk z`@SJ9Bfk_KR7JSGMTE9czNpN<1R{knr^2<@rx_@9vT^ZxSs0Z?4Yz8zxGx2O7pV+t zQLd6*9u7m9xW8An6o_-Xsra83lH+2sX{uOqO^E(lJ!#TkD4~Rlem=|P?3UW&g_sAN zN$~ce2ar|E$IOLk&xWQ&KM%K0NF-tX+jA~wiA4UYChf7SQLZcN=>M3zzpGA2N9ZZL zXNUVQo#mTraSTCB1-Yrnw5^G%0@4oichNXetWsR_w-q*W0B}DT|9By{_$Y00Henc# zPZqXYrm*Vv+nOO6jctNR8T-ph0g?pCq0tR#?H$BE+mdvVM~cyXJ20yk5}pwG+U}cfKZ&~O*&hJiLtoNN?qCll(_ax z{w)}9Uun)E4bjD0C(8g`J&1R4Z&L$#mQkd3Ar>J!R!j^Y!^l{E`#P;`qKLN`|9#fg z%P%ZJ;Q6FGPCnK2w@`an2u)kD*1Y#YtNB6eoGg*7?a;pGXWkA#z+#cqu*)2Bojdn9 z5%eXh8lqp`uPXd{jc{1D-9?dqanopzGX#CRRY%Bnkz2KQ-=~bUD@Z1Zmcx;gjn(?c z&k3bMZ2Hcc8g(aY^P}jD!n9l%Z9vQh(-`6&bgSex|DoTF1@2s@*cJ}l+LL71-$u{i;Cu%0froZ%wp^0S@CTvN- zUZ`wbiHt(p<=^-r&LH42Hhd>w#^?5Y5O`b}K*BG|?D8}>fiMl>JMh6_ukksDe;Q}2 zh{Imma)m^$e9CXUyKFq+ z+RmZH#&qiwhsv|a7k1+R`uDx^jJ!in^X<8ftIT3K@Ykc`F3S!TY2Bsce3=p7kI6iK$~*UxEsLOHk@oxUNF4CQfnRb^N9HU|*vIlOXOItu_`GRVyDVQU%=bcIeXBFh)GGVNJU#-P?r zO=4?%l>1A~@E?EwbHv~(ox}ZbK3vsnNbRouX!{#SE2NNg``z+U(>V?EJ>9JV4H2O_ zfB}#x;pPXw2U^6DZz8NtDUD=OYnRnyT+b%JR8GLK@Q-u#b`C}tIAm(_-qUl$_&+y_ zJvV0PR4lp}lscpE`D|o-e!}!afe2u*29&B-q)l_CEEuI=_wu0!EGP(W>Ar0K7IRvG zj+MdQ{g0bbkwXl=vfz0PRizgIrad%hrCB%_J4hz2AJi6MgS2JY?Kj%LNY?0CD>{6Z zP)nXC4HSGvX6HX;Qz7r9iW-}R1mz4IsLSPff_OA%*H5wB@SO+ z@cgkE9&w3(?9gcJZly@sw2;*0&TwOoox3z#q3g(FQl>(3<0uQ_6R=(zlf7t16o@s7DjXT4Fb(p8LVZpd2KA>PY73sZmrb;iH)Z_hWr;5k*CbE2FWwpi#|9dE{Gc&6+S@>(xUfRhw87a1tbJ-- z*wtK24fe;U@XXQs<13P-3(GlbkE>R%5p>SHsJ_4E-kyR<_dW3fnc}X#QCTpXaD6;~ zx!=iQpVCR`y^gzpP*zFKd;`|XrxfD2oc>#+lP7@n!+)dAPcWec*e_Zf0mLupjkNn* zcUDHsoe-?n3h@k9B_XAMRT(VeSwU<#FDii~ce!a|7i3x{#}p*9)MBmHK1V6k^eMkcb2l*+ z30n_5+CMuDaQN~IL)~7A zFMSZWyt5~sUdpbQOk-vic%n#ym7o z=2veNhf!&ZaY#!cy04a}>#UUGR#k+v0KDv`x%xSUZj6!Lx{)%dNE!4`ch`S>Ag>mb zKFi-6jj4QBYHdCmFi&qdL!33MSncIhO-Rt!5_M<&)s32|?{>f};f2~eD30yYl6)a? zQAt_x)7G3=24|Z_(S8IO;y)8MHBhi=8zV_Wc0irFQl-mN54Z!ioZrma?)l7ABSgib zn7O*#Xq`dkITO>IQ3X=za-t>$Pb!zCu<(Qky>UpT?Gp8WqUZWJ5DRf=ecG zbAbx^Baxjuy$-ei_R6n3I$bzQ+Pf>RmW7570ee2wTS-={|aSChoJ9qSYmqF>OeE_#=#SeX? zF*CF5v!iPXmt=^GDP@FR;~Mw5J5LV~H?fI+=SMHQrxZk0&`Uis7m2r8r33S*bIacS z#nkFH%BbpT$NopyqiRL7csIwD8_g|0za*He&)aJhz#eV-y63w>v|ilB53Yx_pBVe4g`1io3B~x;{d+zpG%~L>*9jdJU}`sZM|!*08~Y2_ zV18wWa@t>GnsX2g`M~HCzG5t~yXAX=&dT?!TfszobewyyZ)s1YzuP@7`kM{jD;GAO zCsCg<_zV>8Af9Fmf;W1Asz&ELeSbY*9-MU%1p!bKri)3)DlVxX={eppo4Gt;LtNs(-NJHur6d7a`(U z8-Ht3N$8sz;2=a0o%{VLqC|qm*P$%D>4D;2KcH*1`2;-bKz`E@+^zmM(Y@F3J60Nh zoWd{UmRw$UfBzDStc&58>YqvAMJ-u_s22f`&FUn8#2MgCjRy&U*8iFMjRFMLHx*}3 z|45`AkieF&5~W^igMTG1oMr}!u7JE+czd~zG+zXYtJ9*cP|q>9=lsv_=M*R4>e7u- zi-t10)B4*L8XA+eV#2-&B#nfSLpCdzxwkeN>1d^73i%@3G%**;*PhbAWkoNKv z9?<4*1}6cI(H*O3q6Bx6A~{y^MbIz7vUhDde~sqggA6k7^C-S#|HVSb@URE@32O{i zD}~~7jC3mnt83x_)Ou9L&l@EAcFQ*aU-In6T6WPoYByhu#s-k+P|d4UoxalO*&lAm zFS0kWaY*g?{@JnvrsCwizf+tHm*?No-YpJ!5J2({^xlMB`J~H$`t^q8*iuX-t~moi zN5j<3y0?6lET7O{68ai@?I0KA?{9=PS`NBO>|!`XYT$3iN`!y4thl8)Estz>+=$XV ztsH*U1<>wUl7aqTvteBaw>~@8S51*e7$x3Jd--WGnLSYPPw+X0!<5(z%^Dt2_~P7- z`5`v&g;V{`u+0~J*YvI`-hcU0Dxiht^Y!_3+SFx@)0V6Fa90XIb}hyCZFPHWjR!d^%dalvwBbZGu*yv*;yDqHYG)fP&n1 z^t#fHw9Cio4S?j|`9hLhS1sHTs=w`s7@gvN6M@Ane@sCLSjnT!jL!lWkJ?VL-=LO| z;+JOi9r6Z(M*@GXiL$ z!0S!XD9KtZkjWroFC*3VU3$-|oY8|D6Hn&yGTaGq2s&U?sRqUHY!zK%)ML}yM~D%o zZuP!yvVR4z+gQnA90UO{z=m!&{f0DH?t&*@Can&)kCw5%?4Cm5Pg5aiEz{{x5`Dw1 zcr@84GEde`4w4J_n!|3*M`n6qEv3g+*rT%(ooZJxHvL@+Df5%<$i{{>D`)?6DQHq{ z(R_-Ve8I_qyWk1ZW%2R80>RJG-1`qPQnAnUvA})A$;}U-73CVoGAt&UMXZJOX%_!e z7dWKv3*(DN=`QxJV(XtomaJtPzI8Om$!&FCd-Yev0xV4t z2ro>BEZ8D1^ql|$kEsK1*JBvr3j10>s${rtQ=FaudXblBuoAP$3h!}Wv$q3GVr2N; z`{gA#q5Zprww0GOL9de(=DbjiSWN8v$Cm7=pLa171SJybWk&`|p}ul~eMkBXkDY_G z`SPW{=1)W-q&We?fD3m`Tg9yRQU+V64LLrr8uwXTBF(E@z1SiCa#kTboD?*wufdOe zni~BqAxdR(e&uA8{O-t%VN#T`IN|u|jfAVzfXPUm3h>-YKHGGv?ufGP{Py=}E{fe& z%Ndc|;pjVI?E`C-1^8mDxK33BRSa{|<{p&$$42FUpvrUxI<6i-zmrQG?E~1h^H$y? zfBQ|t?7B3R>DHc?Wze1d%}x7`{s#7hG$Ldyn0gBvAYR2 zsN4RrtGBFtQPw8d2hCsBAo}c``S*Y^uy}>)*$u1+RG9<44$ety!@|Ld6wwylA|AuG z%b+X#^<@%kVHx8U236WAiN7UX!KH|fCbiQ?`7Z-CT=-hk0Ta98C%%T^R6Sr%#!_`~ zi?Bpf#c$S30<=CBkI|C8KeN;AF#hJ%k8(!?fv-ZJ>|%*RWeY5K*!(Ww3LMAtS0?qs z&Im-WR-S)S6Oq_Dl7`@7`{bC;%PtYuhs2QK?Xk$ijLoaFi31e~rP6vifGwS&=b7=x z!DkWazGVf(z#~fRs{aMmCAlN!etpk0lU;@X$4tF><^^J3t^;+mOLOljXY!`+j67i- zG>0gf_0}ugS_gHu?2vPAlxU=6k&1%Xs$Zs$MJm~6kaNF50Dxye;lP)%HF zU;`}ikA*<=_&J|JbQNgboFdt!#mNwF1oT?a00sm7SBwqc&URgG*^z0n@mqOB%L~~> zv;HIerhuWVM?y4#DwE*^B3e#DU4s9K0@t!*UR@+ge(Ude;J))C1x{Hn$F=4?hOhBd zdv%2wBh#Piv zpZC5jFpemcKj)UpwIi9ldV4-u_-zF%+AaO#AN**UZ6$_)Dol7LNL5z#>n$6fQj#A7 z;d;>OP^L^fygQ0>_kZPhOG)mYcAR`8oXPjO!OB=e_&;Q!Tu*MieXmscbI2bA>z zTd6^7xha{Q5@nLW|Nk+6*NN?4Q70jVc} z)!2h1YMblR;pit+nAqtbkQW+0@=~7y*d0fdWV+(mMqU1Q$6Kb?o$5B)Kxc}EFM8C) zsqBJ`TmeNMjU^?&45>7Lmkw;aLv_&u`&|yYF>F^HnBB?fvzb$SiTl^V#vmMU{I1XT zmqMT>2d^?AeqsH%Mrsm3KcYpLRcH@~>iAU07wYBM`dhb!qC<}_g&*5q|5LZKvoqHf zFL<`eYsn|Xf&&g17%ncYJTf*hul}yFyu2kd`&b$od6IcRLbzL@pVgUAfi%{<>1q z@sYa;j@%~P5K9iw=U^oHq1zYd0y&UF+hqy*N1>-Ir0DTKW4%4ALuVndYOcOee5d=I zknZta>#a36pz@nJuzidC*<`j9WXiX;^bZZz`};niuF$^30p<5cU_^~tUIzP|v~;tY zrg>sc*|9RW$q5?tJM@N|6Lv7V~d-bEC=! ztO`ZF+`GVUE+xf+I_Kg<62+2uEWGw|KlVnODu5o`x6}28Y2N8rZ_5f$L$F6o z9CtF+k*YX;Fm3TP!$(cn#831uo8UPPOpzSLK0q$`tdaVns2}j7f&R+WGxv`N!B(@M z=~mOX!7%8_t4wICaDfk06oC${%qi#a6T^hmqxzIe|HTs z;^B9P2CPUTtqQ6zcdHw)E&v?+zK}pKbMKjl+E>84LbnVuI!3}aeuP*v*94;g21?+C zfB*7RiB&_i`41w!h~z7Ufi7(F(w$HMet*OGJZ@{?3I6{s1&vhNvK#n&6hxYjrSgt9 zm68U|{ss#W^c&a%6){mD0E2pgTq#)OO$0WpL$L3l{|IzX(QwTgER&{c8x!acfCAMD z0w4Ulot8+m2c$f|QV1+go&3`Z#cLiOzy9E{!D8^ZwTT7bl-?r&LV+;=h#+fG(PulV zFVp}N5&R8`zy06j=^Qq^Ud;8sOVtN8L?61h6#rz&{@*|i>Pj86y77zDuD~D^khs95 zilEA-4q!sSFaQ`7bid+cz!o!=h^%U8P{`wgb!&i68OnC`3OBGX^DWI;2PeW!u5d_B zEog!_Fh~m62l1UKidm7|`G_y->C%`aqp4VZ5!d0pKXf=MScPnrv<^f;&qoW4Y>eBT;Ub@V^?$~J%Th$8>JqAHU)pB7iA5nH}MA$19 z2umlb=UQ3JCwAnYh17+Th=GAyK#k{ED>Um#=`v_2UEZn!5o0n`A5Ai#Tzw=s{!+oo z7LAm{3E+@L=}+?m699bsr)FJodEIz7`CxzDf8AI1uznqYt&l)wtoz1#=V~QUVT*`! z_zQy76*7sjhM?6bIbI*o3g7>$4Pq#ef%Pb*e=j#8hP$nFmiSNN1@^}-K9Vch*#Q}P z{qnAuP|+w$w1G*i9}INjTYG>b-oc)y|pM*8rX`C+1n zRB3kKKI!O?=p68iCBSl%Y-Wnslvgj2d~?Lyn0s_&wplxWN3G*Vqpev2s3J?v0Ea3v2 z8?jzQ?_pxaiWFI%X>w$79=BI#Pe;Ibi`Nromr*>YbOn{NSJ`L0M^^77&r5f2`R5*) zVS4Jc1NU+RW$?atDF-=GCbgaWG52uV&6rK^xlxh1l5`}n1B2P;v1oed=ugn@LX(>% z6>)_Fcr)432Y1VW4hIJ2gq@?@m*kizznP{oo*dZ|Gvs=w_`Pc_!B6=m;OMivM`Dp?KJC{r(qWDD~0+ zjb-jUI(?&C0_V7Yv{^Sm^NBfNt7c|>e!o?F{o!E%s)1ZBTi-4#pR0;wAs;SkJ{Htq zoRvm1vXJGJvQe@+E4)O~GN@JPQ_10O68O7xg=LW%JHCyn9s@}g`#-#jTruxubaXHq zoATJLTipK|r5mt$jXOj1S!8`Est}ciI_DkBs8FUxN}zNtSDA)>S;f@4tt8G;x*L<% zx;`LpMy;BCJ}4Z2BMAmpE!Sf{2}4MapO?eAkMux@`RZ<5@e}$>stO|e^JZNQw<)$L z3Tey>yZ?9>pBhQoT4UK-DLUS9T+R#FrGuPed6^k=%ywBx0IOdFtp2WTCSV$}trZqS z{|c^rI*Hyr3s!6HDTVHEA1|y*`)x~a7$^Er#$-jmo3QNNLsEP2`hb%xXZd!MwB>xX z5Ow&4Y7z72FJEp_j=erYiEuiU~0^2CUbYisNjA~dNy<(h&D#!WJD8I(N`JY5VmaHSP z;?evZBpI6lezRW24ZN+BH5vZVe{^EzpN0)(*S4IWx$~$guP)T{L5k`Z;%7qG;$ZM6 z6uKe_4{b9!0ot`Rc^F?aQxiVE{TBq*ydJ#M;{kbV#c@-PdqAnsjyd$F{|5iAjr#@d zNG+A`>$E+uIM;mcDX)v1rQo)F|1{H8rV-)etw7z1iHr)h53dNU8z`)m zQ}5O>7kRtHUr`U&1;s*k+t(WyQ!rq z6Q6#z@76~y?>IzXGX!*vC8jd!4@>KfChf^;(YY&ZXl)QJTKp9EGJNz+>#+%o&(t+Q z=nh~)Ti%B196dC}7cW`qzoHeZj~kF%Qr`cj5mpDb7?X4^$TR0(A9A{GRS(Sz`Zj-{ zlXN^>!5unoqE0vNe7I3U5Z4VCf7D;&Z05i7vjE3p^rwi3Gdgj9Q9mZ~#<&o^{M2Xz z);Hl^uUT-=r%4t@4w)g3o=@l=W6YkXyzlNxVPNGX9uN#dLzbL$k$ETOu|AAD0cH68 zXWq1eMMw^x3Bl+m>*suF95$bo7R|*l%7T$8z6ER<30!WtuP3QpbfCBaL?c&^fg`{` zoiuU!!mLLsW7ji{OTF{YdHO{Ngh`k4GQqsK>}i*n^i)-8}^&wMG!By_bQSK&{i<<@Ed0Tu^kblzs5)=`~N= z5cn#nyu9P7S{9ZeQ`B!J!DDn=uAmO-&aMq@x-Vt4M+F{C+o+BCG(v$0(tn+K5^|$} z)E9{!^hTcEX$o)DLB>a9&>}pw@j+F|Og(O74A&3UFEthUEp44XvKX@->z_@nzvTZy zb_bUl@O~-#lUvF}=l0>=m_w1LO&|?e6)#w&fU124v|O+Q=1UD!cln{`cCxeJ0bu@< zo5vObE5>zj-5)8!eNH!3$K{w-_%)car!^C@PbFsu>aG-=6j+=SBRtJnSth3r%yJ(| z9tl%@^r4Sa6>f~fm#mdWKX)UDWuFb76rOYK%PzM^Y9j0qd0{099t3iQ`F_RlYpF6XQXc8eqQ6LUwc{C-yql)s zOoMrTl*pjQ&`%sU&U1J^n)3Ye=v$IZWCGHnc3SVyf7IM9g76}}qmkec*t7l-F+Mh8 ze$gd`5z1bHUv`|d|DK2Y-#@-v?GjE9bO$sE$D8P#~ zuFGl6^01fc_1Acyz6jm$8*1|s!{N-bn14dW#)to28V#{D1#ToHkL(bUoV5(qE|o5%{y`p9`TtsZgvR*xPNl}fTGH^o|FE4 z+aYB>I!GO}QR*>pJzZsV(%YxL3-^9C!0FKRux`bfTq}#bS+-I%I}i5>tXKifV_{`# z_dw>T+x@?d(Y{ujbgvNv?x)cf&-n+ce}PgFacjM2(>sj$zJ!V*Rmd?WT)S42zxbNj z;$mE9JH$DokiF>F4(fGz3eS{my*1i&UVimQTlb#;pjkjO$=O@g8dfk5Z}D5cqV`|X zRt9fMx?Qy8lo?e=SLT8FlF-gt>eD!i{iak}2-m9^2YF1*A`ko`q~T;~i0a*B-<;1r z+4~J=nY$|2o^@;{B#Q_XXs`D0A6+J|4*lfA6|v86l1NJd67`L1%qg8W+9wFF9+j;X z`OBn%aJ5+GSXvG(&1yfg+esH4`YvdgL6XEj0U%J5bjaJR&p=?VvcyrX<~`59(aJVP zUlD2PWgXcs!&XmRBTMs&#ynpJ7EDs)MAqw`{7$*l-oPl>An{;wUtn`xf24N{5D^m+ z#!_<&R-a!+hVpDuG`Jh!sd_BGx5Th!tIQ1l8p#?nG@F4z#S+Z&UyRz;JTKC|&dbj< z>f|*>w>2;AWbM1RVyh@S>TR=VED>H>uFHnAtUUiPl8T-xo;OC_0^w6DawJlky0T3f zAoEdm$u+{aB$qc3wUj&av73{tmuB9Htrme{Ef>Uo){J5mwr2n)sv*`(53B#TvAzVG zZKtax>xfy{Zz8$Z8x!_ z=;YZ)(HA)0%=~mfVRAAWT7E1u{Oz}NoRdE9`NVZ;|A_qBuI{aa-Lk*}x8v=I8nMam zmi{&IEL=TOB>XJD7QOe)gxCL@<3n;t%C!tgoclB0Se0>za!~b-hO3_{AN#1r9QJMryCLl zS1DWg?Mtccxrlyv^F8F6wMl+`PT^%wz>`q9UI3B~lJZGKl->aKpq!A0n-feH3*(w! z_vA;Mh(hDaarfHbObsSa409MigLmSY(#_HsDVJfFs4pLND%Ik$KI&Tc0`53YlmkCu z0EmHh5xN(@ycQR*wgoxAYR3;k&F;eA6BN&?;NbiOiw`RygzOBX(}}?U4A?77Y^>+> znYB=7Z#BUcK=y04B|jIRo)oP`t~Nj1Oek99M7&ko*If|447hL?C_p@sEVtCu?6frd zc5J~Zj#%LMnclv8Phx59Iz&$>A_wlc7n-*dh60^^-ensh8ny57sT<@pSnD(ubb|x1&Bbl1PVUTaeT^_TI!dQ^ZK4j9m$l*g#bF84^-?0@tq*?ru($9nDtsu7AM4 ze3}|O7eD_j6wv_U2J@{W_XB~=kG)`??#CmgsR~xnMIRqf*qIi5!Qv$yNbR_*fp${9 zNe2FYAXoJ~g&fB7AMx!^dugb~Q#^+YB|LR<3n(Dd3v$&EngSM%Cg!#&F8=<6m)ymo zfM8R+0A3#$Jr3CStBS1@z}s(!Giz&PD$-DD4CRF03={=YmI0_3T}x~U46xxa{>yUo z+gwv&>DFK&bGJDleHVQ4#>5X4WOW>2y`tOY2pUU>;N3BF7cnm(?pUoLIYbq9pioQE zCKIqsRx!jEG7%q;4UVM2qIomoI=~bh8Lt;PFLp8Skk(4;7zf7*+YXPtr8<|*%;pK4 zJFddNYF~d%|6YbusNJ5AcCo4_4h5Ihd4c_Oycv!wI^^^;%$p?IjGAjKYl$?XCef0k6PcXUy#wuhVS!;nI)D z>FFM5$UJ87pf-@S{!z?$$m{Vd;c;-rXmmT`-`xnz^l|wNK$r;NiX5Y6 z{gZ72dm2rK4rK@&d5l4`zFB&}xD@u?`|qD$4zI*rf3?u4I$C6wepDqqZJRM&P_$&F z6=Z4iv}LtqeVbwS0U-jir+}YWu~oj-e&z~|{@XNNOfN^YPbUKmpA?)B*r{;%AaMHu zAb=b!2U3cXkG9hf2OGk6{42k#USBt*uUX0Q*iBlyb#aI3pRKpDa|HDD*3EkKef5$C zMy3^n5BVebViq<LRlrgq1j zi5u&S$ZpvYFN1^zOSIqmF(f6dY}D{2fN|@~YYkK&$+!Dk!rhOR*vAxhFOvbg zBL7VOar1(;e{Zil#1V7l8taB<$?=7EL1sVNPFi{EI_-Ll;To`i`)Is!L!m1=u%z>3WG<^vV&ML%66_IVPk!6J$tIb zB&N#adKD->d&BqeIM>Q_mKWZCp(&jS>Cx5m)(b^kWC zlvC>3k&Z02o$IM2Qb{|fgTiJ^lp|Z*J0LMipHdn{FCmh0;+@#(qj}^?QjIVhs6*2e zeDqZ-yRz};y8$VoV&3ZmQDncxta09KzkAy~ZI;x#Zx|4@Dnq;K&6aMPJhARoNf67S zMq*6TUWjCCsILt5SflZk0gSS%Y-MtxA+7dXUSB$w%L%&%B9?d71B^s&O8Q5ndIu5& zMleLA(;nqZBQ78cf^xU9Kty$uz!~P@b7k6ECc|`*Uk}d#By*N`Jv?;+%7Uq6AgEbf z^Wn>!)$7IV0_=E>7XynHF;aO+0&XB;Ehs7rv{kg-8FNOd!6+n)#W2r%Asu`Ypa2D5L>O&( zHz&ccED!?M0Vm23>N^iPWT+c2@_;N6wAaq>hSZA|FG1#*0!mc{I53jG5*zN;17oja zNS?u-+SRFJBTza|pJvl;RXP`P@&@W(x zAF;#2dgeCgDYyz~Xa$6{AS=1zDMkUB*nu?B^PV4Mx$lDBGT@IPDuV%_2%VtOy2r9Z zJLeTT@M>0&Uh{a*5KqpUtSrztu0VYM&nejgA^t*Y=iiPH(~479pbEsl|F?CR`73c@ z9V&~IC0>1Zd7VOfOze>X?J)3n7Do*&DGH`LlStt4W;Jx?a+00K_Kd+Hm=mEm)SdxK zrL@OC47yqV1A1-k6*>8Sv0?@--05qrVYi|-QBV(qA?_sLlMPz2St+24u=A*@c5t`W zWX3LFi~bPY>nkXbApO(mYY~31E1)DyYYF`jSsb|6s1_YUH>&|SC7mpWOVj45G`$3H$|PO z@2)qXT{xpraqlSm{8N?;H&E(?8!R&Ganiqo%P{Mq@7}_4{xp9euXab?qGB92?1d{k zFd{74_y~M4lI?c1&Ap~Y-#_#CL=)QM1k9qF8WAL{rjrvvHDhK9lnelk^C<#tJEmCQ za?|AK{^$x20zvbtU~r-;jg>kq$HPRxGI4Ji!@ljC0KKlF`oDH*>+yJrr~&DsI!1b2 zobYY5E4VR`83YM;P$)oiYT*}xSg1b+V7`miQ(uG?8}j~ntjtcXqkcO7nC`jy)MvQj z(EOhVWO*;W7Kt6VCuOuRmS9?s&A!70P0Uol9>)ZFKnp@`ubQ<+-ncTMGf7KSa}1!- zqdwY)pQnb(LQMUxRpYP35Gi$*IHqV;_lz5jz9_Anc$gNdz!p`HB?`8_mXV81&)fp@ z&!3O~q=%~iItS?c<{Sjw4&QanBl9H9HW557@XfUdbW2UiKC5xhCe1~YBOTe0u`lkPCW^T94mQ|Dd{|r+0(y;r4iC%FSL@vPGT}Qv zX>99zMG6c@f$-O4Q+kS&f95SP6uyGL5>p=7E^_L%i1A+e?9jaw7p+@8CqS@Oe~^(1 zJpZ@qh@SW)zjLN!@PJ_x$^ZF3FGNgea481(EiZ$5b#>_5qssdIp7+Z#gVX_74j##0 zBb=`+sFjVYam=Z%%P?d8a%Lf4%n$C(PzA!I?h@z135DO4~Eg1ECk@cpi^PXIO%HK4zo$TCp2 zI)d*|b7vCnRm|w`5UTWkGZf}R@W0i;!!Xz)Mn2WPo%eLiA>P*BWiCh3B>a`cAsFO8 zhL>p&Zr3P#JIhoSrp9lE+Y+fmiWvxBW093snSq|4KH83H*@5HJc++op)YkiSN4Z4^ zdBeuOpzIUPdx_Am!P8%9ZO78jUXrE)?ITo`lAitRF2HD58KI?R!Uc>%`U=>u-u~Kk z@5tWpK3N5ZF_KI)3C~VeKCgAQBk`hhg0W`~#^LL7)~&#t&CVn9!*4T9%6C~|L5R+j z+oAmhtq(!3M7`(=7q;ZmsOQg40Bio5r{@^i8SW7`4|3-seV zE#G(Ef{p(!d}>W?U6sFUo2{Snkgdr0bmI%;$5PiXW8gC#-^6IR7U|dJgCC)O(jxBh z;@ML^ww8O1_O-BjWvUTcV42tyjYB@QHlB^D-pDXi+Ln4*3kHLK=z0Utkw1ZR$#YRnPpl*C$5eTO)D7PX|NKrPBDG+m z<>eqq)s$1Cp_Wt8yrE$Iv-E9Ou6~@+6Qqr^^ZIWb^3d3yKn1faEgU5hS>mYp@(bPF zFFb4&w73Aiv4nz!4rt02-)Zlk<9_NSEcrTHd)~z3eXFRv>)&N6mc+>AecdnX;}vTQ z5QdD3oA}c`ptDR-H|xc(PNk933JRC^==eTYa63IaggIS@t9UQXA^D~lFJ+kTpn9#J z{yiI{6&Da@@5^dyeRtf4#UDI) zu)7`fFfRc;#(N=16%3%Z{4*)~&;~>>A+h*GUu|uOb-D5LrP7#0roD=>?ke#QeW10^ zZs#xef}v<>i&x@vubScxK)hY=MZOVV^xs3Z#Up|gX8U+A;@h?u?iGbtkNh)zSu1?( z9X<3mbYS0N+27wcAleoz4amE&f$yUGTKrcr)!x(mt{Edt6>Y`?`qNnqWG!x1?h}$q z4*fj0Pr{1*cXxLSpT6j5cE5Eeaf?dtGlj{;9L$(({mhOmIr+!I3acwC&-~M0UxF<> zRGt{U%^6C2-F-;PdGeWGx4|fnvIJoG-(WeB2EcA@!~!+qn|n}R=U%y4Uj-k{nzG^B z!lCb;PZ1gB(iY!B1=DO{J~7^g$|t77LbW8Vz&m(&)Av}f^4+F|_2qW2gD7aLuKP$^ zTbnjWplhuBDMd@IjmC6J7<|L7yRD4dd1xnjS=rb>v=*DC4^z~|rQTRS8L(g@T>jX} zhX@dAOtAfapBHwI3v3kP>4Ux0iC^_+%cW&xuEL;aapK`0)Xj{M)V!Czq+|ue0-{fo z0&^mq6+DHCQRPWBGCZxV0z|GU{8__WT6y*p0L)G)o+@i_WUxP5ipdzPPK zQ(gtDx@UTx6v$!N0Ci_D24zyv9eu{By}f-{zA1|r(j{2Er=I|nK0-&Ps?`TZ%D^l7 zpgeV&B**jk_h`Q4R_DpOl8qDIL$44ho6B!(#)|3#C}Sz-VHWS*9%mE=i$}@I$mr6a zPpo?)f;7(7JAR-u4?~BUCLx9ILe!fq?a$odriWr zr@XPT=*~r0)76>Mdvniso+oE8^o7?n3WON9_7er`)WPZ!uyBV~LVv87Xd;O4gaiR!L?o&BajG>AJ}_DEkr$Lp0g< zWqz;u{J#IddA#4R^E&7Ce4aJG$~sCt?Ir=hp`t&*wDp$mzoQ+u@(h(t#7jd763Zb! z|4*3YZ$$sgD?DKbIallA^kmgDa4P|RtkkK+u`v^z7=#m$z*^#2;qFJ4G&tr&*YcJ; zi{5ld9Aq&j@IPD|(5P0EoN^o=P9%V)Y%M+)^ZEHuG+C-*sST(Sob|!I>sS9pV_gk9nf>Ue z9MU?RXJaAQt0|iWQf3iTjUk2T6!QZCVtmkW$18I<`HH-}JlP(0-E4Bxsn%%xFf$%7 zoW+^*Bc_7OW;{_xWgGNwzdNl$v7uhNDo9GF(AvuDmN{!M=>2uJ)wsUxYM=GB#z!`} zCk8cPg~z}_^k6_?K<5c{5)?)tpJNN+za4%`$j{8oB-#J_@S6=@xg&&jesbebC>(k} z@M`9ThA33~HA>jwAgF9nr{vx_tEnoo(;RZ9?js*mn0gsKa7nuIdJ8)UV>A5fq0ul; zKp28p4&r2CeR@H1!p)wB{GF6rTW?&5d=7_z5Z=T1vJsT}7(S;BS>6WU$+59uq1uyJBp?T~FMl<>;UqKmb-o6|igpscnPB3+mX9tk zu^7oSqt8LsLvIML6jfet@-Lr^KchZx*BV#}f4N=s2Bbb%Xk<%x!SA=;VtEqCPHj#G z(N;3u{p-xp>T;Fk6Osc}m)7Lqbdl4H>sG#U7;`5y-iWFWEah2gd|hSbnx*WrP3pDp z($lt$6;7(=&i=TA5Ah_O_+Oosa!xw;;^bW+I)RFM9=nK|qYy+3v3n>dI@VeZ!Wd(%|?WDPr~{+U6|LLc?+bPA7nSVpoikL>S~6>#BL6 z_cGCwxHGaR#`QfA&0Qs*{Ezn&^r6c^`RS7Z93i=VFo>cd)JzuYNq{@TmGlVWi0Q0V zbEe1sboWwUTQWahF3iLYP42lYMzn1@b4ut3qb*GDNIWOl+Bu(4<}C{lP_FktU~71RAYMWtYGo!3_!UEN#-F|V{%WRCUZAW@qJ8%& zcW6PaNxu6xYzFqVi(P(+?GNzLPa1QPKVF*O=AP`_3o(^1y?ONQ+c&@ZbY>9S6rz}H zDUJOD^lWDK?CFpeERvdrZ*OlGgx2?gwI4LJZGaR59!wD5CIbTpN7>FgZ~oTSoWhoQE~;Mf+&|0zECA@A|-E)t@kTA?5}{U*z+`6Ib)nOZ`l z7%Xw?rqE?wt&|n^$B!|q0uXb;^NPCP%PQlMv&_|gqD9K7cO^X|HTT9B)#nS0WN1H3qpRdNbNP(RY_TUe9nLDr39%NW}TIx;kzUV(JiUnnI?1V z$9x9O!NPOOr7b1gw^Ded9`j-MCliM~CT_mX=T(EC-^cNdYXHXVI0YQ^$>579XfqG# zv^jT|Pa9&|{QCX7&icBx-^+z<0i1Q>kY-k9X8#sUVd!NG8f#g6`6W$s+1tfY5c+)# z^un6DO#XQ-+!n5O4)LPVmL`!ZJvt@`g{k>#BqzWv{Hi_G`3W@OEPiqI%z>+P2L(u^ z)G9MoUGYe&Dfvu2Um!Gm0dr|a+066x`HkzifkOgb!LbGhy=Ghvfzpxyzv?_EZF;kn ztpo30yvhc?U)(T;V=t-4sGEsIHXo~B*yan8N_c=)t@V4&?~q1lWbdO1Ry*VJ(_uo8 z!=pBX0N-jI#0nqb#V=S-f{64Q)T=~&I44N@OAXOz7M_c|lRPUZ3e9OFTJna&Ey} z30Lr7KYtt$8E2ag8^eI_(SMKO!WGOLv&P5A6NuPimE&4q%j&=0Igz3N2-12yOLvJ`&;vF6n*azXs*G<4j4AVYMc#@tSaS~*CIz*< ze{nsJV})vg2(iE5lmGgcC+vm%LSI}nBRp#^i06D=2E*sa>}>^!6dt_-#kycm;cB(^ z+nYlFeL+0teS?c!i0$alOMSPrk{B3PJBJ5OLxN#E5W9-lSEp3HT)pm6L(q#j}8cY=T`jGC_uhhL+SB8yLt!xy|{10jEg0g;TaY zz$@A-zM}=1bg@NzYP)Hg7kR+PfeQ^O`@KzkY=LUgGk}yQxKrHn>)BH9B1f@Shcm!} z@X{Tckj02NL-P(e-r7qzpUXHTKDK|woIy7 z!lF$I0MZq&`05xf=q}|Js73x@oMJ??p1bWpsxda4M-7d?OyC< zmiKs{PI05~K||{CjVs@hEIee7GxKl8ke!5eg?J zw$Bx3gfj;Q20%HFjJ@5>(`qoxHB97Zcjhg;I+PoDm#S=C$bu2d64Rtlp99XUrM;sg zr!U7?W~a^$Tl5Hn?aaYO`Pr&;XaE7D3Qpbm{J5^fp*DO#O?`bdM@tNvPVq%UcQbJA z&O&(2rb$&?3w!JIsO#2;U9HjSd)b=sGp`mn2L7a6@s$Bq$n1bXztMmwwKDz~$E-$@ z=$}My8>$R^7u8N8F|qQJFk)1JGCauG=c04$Bk2%vcVs%*?eXt zk{K52%mb{c$kCc5zj3$sYKBV@#Ea;FNxo$;5EyrVa^HIe{+z6=fdjA#yWW3<+qz@R z`9)?Uq#;r^ze|o$WcUKZ3Vci`Ri*0z+EGZ!Ma5wcb~s^UcO_v#Q-hFaNMY@2F^^Lp zN2cL(#qW$8$HN~4045$%Ie0^Hj+l~?0>e@8>IMk0r_Mbvs4HnL1a&O$6&BU1Lu-a= z-1y=cJ664>?LtHwz={wV0|`+Wn2=ss8a6!U^grPR$+}vE~5^=H{_pxJ$&{Z zXBzFL3=A535#^~MCs$()(}=GH!iF{68)U$Qv(@7CVH0u0R7kCNLX?NQgV(N9&PZxW z44x+R`)H&Hm^37eGkEg~Tb!IEo52o*j9cD2G)y=y_4>L%%y3W5lSj!l8GgFtUw%AR zn`jO}m8}A_3eD7=+@feG|M&Lx_J&3M+81?hw3|5n#vOMFl#K}zv^rH>3Bcud2c)nj zhL7-#Re=}E&-CBK90LQemwkS{l$%M!BEn`rvG_fLn<$sbt9h^y&4c(E0aRLCjBDFe zY<=~@TEOO$)*19Df_~QNgGuz_a7C`OazplYA}fQHg1X)cNHRmEGY0P5-!m272cIW0kG5kYSjcEji2$PhR)k zz2=dXR&WrdW|J$P?M&o6M^`gE?_o$CJcRsPnM0aktt3)c<`Iy?(;7EkcUit`XWNMF zS+ry8|LjkAt1=hc*l9C`PUoxrpWHwPc4<}HX}Fr>A@w`oe+caD%J8<6RVH$@G|DJL z=NNDHKy#G^cdief>S1U>^FhUvR>cwfIP#gX*V9hHeruX?7X(n_s+|b~gw^Ub+qB2k zzuO}W3p5&%d*Ew9(}=Fqh?YGcV1$yvDC#!_zOmil*E=lAX?x>m4Szw*op0U2s!pv+ zwx?hUUNv6L;rS&#kn-=K-Fu-K_4AhQAKY5zgfJG1!3#q+5jeSKPuJuud&@zb1;(xT?iJs!d zP&I(PD^c;MyE+2LUni|4~F z{9_GD1lK@MuOb#{FEGcsHn|b7-&ivkf5ov?i8$FjNpP}8A zBm=V-rSpRThTCFWcmNFJJouoS<;}@}(od#U`A(nlS1kA7BT^>Ec$Fzqerc$=E)cf_ zU;5M~wHWxwI+0_1h99nMy+Vl94SQ!Z`CizXSh+ld%ZH zeE=c768RaQ&-~Eb<-d%NlI4bb*F61%UrkCd7d@}2sQ-TPi(9B8dYe6u{v50+KKSIz z)5qer;@=wn70iEUC$ldI-hXD`VTmEt|8J(*?boU2jW8ne5BPGlMBkTS~+p zr|zfJxGorO$g@7|?%lOxirP^p*tO&&Y#krnMf(2iNPWw7lR$;9QPg`uS)&`#-}{!h zKLQ2Ah?_rH#ZV;PeRC$nuT+$-9~@G!n%5g8Ja#KF(9icXKNHhZtvUO=+=b@ve*ELt zhn`l~8h1}I7xBZz9)XR{_qirllI_g`@L{<*qDXe(#?&Y~Ct7+;!B^OV6bP!EydHL5C9fs%z6S@t5SsII*5 z3=vloZ$vzD5hcEAYM;)Q)%(d^p38_n9^?&NrW^k-gt0at`GZJQn9D$7$#?)lbT(WX zQ1?MK0Ipo*zV_x~HEt>?F3t3uG?gYEY>B!$IHK#A;<+s_-SnMzs6T~9^ft#f>wa=? zOOs*@3KNb9p|v6z_T_YNvIVlorAHY&;ra*UDm`eG1|U)38LjG<6yd9bZz6t)ya)Low?EU(E#ff>GkawtX~w2 zjr_@IaR!lwJ-7({ValBg(F4KT$zxn_Qb$zOl|L!Q-qsUHVf=n)o>_au>|Fld?B{ zTiVB~R)pnBWtfuR#VI*C;mOL;u>%qI?s1gv7PR9&#{(bBeJFx-N3>L{#j8eTU8as_ z*&kBogWCokcLudT+!8m@1ncg%PUhID7n@yD_7gX~O8ne6_+8i0A zVz!^K1H*^{z7CD~NGzzhV0uv}L$yn$jvONT6v+1Kd$v7^fJjv}Wu@(5VE)giKABXX zG`0T)b)a6)=xFW?L1R3)(Ua&}gi?_-f2Y<}pw(b8^Rb#Pip}vpgbRrPqcHjg-4&2X zaR4n4`@u^S2ViVMj&{J*c+h^LYE^MEmS&%yj77bL(TxCs>swtflk&_#PJcqhqW>wS zWB0Vto7TNxTK)`~RKWTo3lEBvuR^4Jn+!9P{Nk*#cR$!CAnV1tBrG3#?7RcV)~_EgLsDqdj{Sq^uY; zY~vCyB3(7e*;e)lQ_n#_&rB}~<{C>E@;IVDa(aC|44P(M$X5K2{QdbECkJIV7bj(w zyD>$HDb~^G_&G11Z=s|@WaZLCO`-vm7++aWl7Q?k03mMo@87rI+LV3`6#NSgOUc-7 z!e3TH-tE}5-<-`Lm)oEnWjMc1AAc;EHwA!{xFaSRq(zGy2=G-5O~ z5kfB_P}bMk@u0KMp>DOL*;}rYVB67H%m*Vq<(In)C@zoa@RBaSZv*QMsOwu;p$qy| zC#Ob_*^s5n@q;LkG7%8#dMhSrGKyZ4u}_2LB8ByOJ2zi7m_zGVQO(e@rr)Clg-#xa z(Xs{Fxwm+1R;1-Wr@IbTe@=m7tgWfMM}id(0EIm+d;CtwdAu}nw*Uq2tnYj0<`y{` zunlLTJ&Wnp)h89UQ_xWe%x6b>jS4hPYlY^Ig1cG$y7nTi#;w$~$sXD) z1<052EQj&&%a`Y6r*Xa#$s1m~E3;j~0an~DEz_mP8$Y*~wo|ScZG`S>&P9tlY5PB{ zq0e$Ed8;B_2esUJTikli8^|tztt-ZY&}I@1T`XYDcK&cw1n4ol@}I~$dvs<_jzK^F z*q>BPQf*c_yTJAYrRH3zWfS?4p%Za;BDuRGdUvT1Y=D?D76&5$M*)c;%{iZ;beM71 zBI-7_>Md@=J<$H7LuSI+8&jv3EOMi>qwD0S2g6Lg_6v69gF91i14#0j;wtRxgOX4J z81J>h1iLRa>)2B$j-&OxBgmKLL?e6za&9LxD@zPy^^>rR@*qIH5qw!a;}F?JHvfVK zS83PFXg*q(yVHAY-doNHY#u$l6AY?rq0f8%Eqsf#q8OTQkC!)c^zU z#|pQ0H+X`Ipruggk2+NHdMsrv__9uMrjd6d1gICc*?-Ccm zo2Ava7w+r|jYYR}k8Gps`|7fDa=5`b)tT?r4YDMRKM8mX)>3%uZKE}Ls2|`f7R+Zz z=Zb@}M}318aTo8J6Z_?Yt>@W7B|zQQOvReL+utQ?1tBvyDL_H9pX khhv_*5qx_{)$4(u9;jR2IKSY{EBQ0KV0pgeEbhVo1Lh?!mjD0& literal 0 HcmV?d00001 diff --git a/plugins/stronghold/build.rs b/plugins/stronghold/build.rs new file mode 100644 index 0000000..e9550e2 --- /dev/null +++ b/plugins/stronghold/build.rs @@ -0,0 +1,23 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +const COMMANDS: &[&str] = &[ + "initialize", + "destroy", + "save", + "create_client", + "load_client", + "get_store_record", + "save_store_record", + "remove_store_record", + "save_secret", + "remove_secret", + "execute_procedure", +]; + +fn main() { + tauri_plugin::Builder::new(COMMANDS) + .global_api_script_path("./api-iife.js") + .build(); +} diff --git a/plugins/stronghold/guest-js/index.ts b/plugins/stronghold/guest-js/index.ts new file mode 100644 index 0000000..ca751cb --- /dev/null +++ b/plugins/stronghold/guest-js/index.ts @@ -0,0 +1,768 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +/** + * Store secrets and keys using the [IOTA Stronghold](https://github.com/iotaledger/stronghold.rs) + * encrypted database and secure runtime. + * + * @module + */ + +import { invoke } from '@tauri-apps/api/core' + +/** + * The name of a Stronghold client, either as a UTF-8 string or as its raw byte representation. + */ +export type ClientPath = + | string + | Iterable + | ArrayLike + | ArrayBuffer +/** + * The path of a vault inside a client, either as a UTF-8 string or as its raw byte representation. + */ +export type VaultPath = + | string + | Iterable + | ArrayLike + | ArrayBuffer +/** + * The path of a record inside a vault, either as a UTF-8 string or as its raw byte representation. + */ +export type RecordPath = + | string + | Iterable + | ArrayLike + | ArrayBuffer +/** + * The key of a record in a client store. Note that the store commands read the + * key as a string on the Rust side, so string keys are the safe choice. + */ +export type StoreKey = + | string + | Iterable + | ArrayLike + | ArrayBuffer + +/** + * The limits applied to the connections of a Stronghold peer-to-peer network. + * See {@link NetworkConfig} for a note on how these definitions are used. + */ +export interface ConnectionLimits { + /** The maximum number of incoming connections that can be pending at the same time. */ + maxPendingIncoming?: number + /** The maximum number of outgoing connections that can be pending at the same time. */ + maxPendingOutgoing?: number + /** The maximum number of established incoming connections. */ + maxEstablishedIncoming?: number + /** The maximum number of established outgoing connections. */ + maxEstablishedOutgoing?: number + /** The maximum number of established connections per peer. */ + maxEstablishedPerPeer?: number + /** The maximum number of established connections, incoming and outgoing combined. */ + maxEstablishedTotal?: number +} + +/** + * The addresses on which a remote peer can be reached. + * See {@link NetworkConfig} for a note on how these definitions are used. + */ +export interface PeerAddress { + /** The addresses that are known for the peer, in the multiaddr format. */ + known: string[] // multiaddr + /** Whether the peer may be reached through a relay when its known addresses cannot be used. */ + use_relay_fallback: boolean +} + +/** + * The address book of the peer-to-peer network. + * See {@link NetworkConfig} for a note on how these definitions are used. + */ +export interface AddressInfo { + /** The known addresses of each peer, keyed by the peer identifier. */ + peers: Map + /** The identifiers of the peers that can be used as relays. */ + relays: string[] // peers +} + +/** + * The operations a remote peer is allowed to perform on a client. + * See {@link NetworkConfig} for a note on how these definitions are used. + */ +export interface ClientAccess { + /** The default permission to use the secrets stored in a vault. */ + useVaultDefault?: boolean + /** Per-vault overrides of {@link ClientAccess.useVaultDefault}. */ + useVaultExceptions?: Map + /** The default permission to write secrets to a vault. */ + writeVaultDefault?: boolean + /** Per-vault overrides of {@link ClientAccess.writeVaultDefault}. */ + writeVaultExceptions?: Map + /** The default permission to clone the secrets of a vault. */ + cloneVaultDefault?: boolean + /** Per-vault overrides of {@link ClientAccess.cloneVaultDefault}. */ + cloneVaultExceptions?: Map + /** Whether the store of the client can be read. */ + readStore?: boolean + /** Whether the store of the client can be written to. */ + writeStore?: boolean +} + +/** + * The access a remote peer is granted on the clients of a snapshot. + * See {@link NetworkConfig} for a note on how these definitions are used. + */ +export interface Permissions { + /** The access granted when no exception matches. */ + default?: ClientAccess + /** The access granted for specific paths, overriding {@link Permissions.default}. */ + exceptions?: Map +} + +/** + * The configuration of the Stronghold peer-to-peer network. + * + * These definitions mirror the networking options of IOTA Stronghold. The plugin + * does not currently expose a command that consumes them, so they are only useful + * as type definitions. + */ +export interface NetworkConfig { + /** The maximum time to wait for the response to an outbound request. */ + requestTimeout?: Duration + /** The maximum time to wait when establishing a connection to a peer. */ + connectionTimeout?: Duration + /** The limits applied to pending and established connections. */ + connectionsLimit?: ConnectionLimits + /** Whether peers on the local network are discovered through mDNS. */ + enableMdns?: boolean + /** Whether connections relayed by another peer are enabled. */ + enableRelay?: boolean + /** The addresses of the known peers and of the available relays. */ + addresses?: AddressInfo + /** The permissions granted to specific peers, keyed by the peer identifier. */ + peerPermissions?: Map + /** The permissions granted to the peers that have no entry in {@link NetworkConfig.peerPermissions}. */ + permissionsDefault?: Permissions +} + +/** A duration definition. */ +export interface Duration { + /** The number of whole seconds contained by this Duration. */ + secs: number + /** The fractional part of this Duration, in nanoseconds. Must be greater or equal to 0 and smaller than 1e+9 (the max number of nanoseoncds in a second) */ + nanos: number +} + +/** + * A pointer to a record inside a vault, either addressed by its record path + * ({@link Location.generic}) or by a counter ({@link Location.counter}). + * + * @since 2.0.0 + */ +export class Location { + /** The location kind, either `Generic` or `Counter`. */ + type: string + /** The location data, holding the vault path and the record path or counter. */ + payload: Record + + /** + * Creates a location of the given kind. Prefer the {@link Location.generic} + * and {@link Location.counter} helpers, which fill the payload for you. + * + * @example + * ```typescript + * import { Location } from '@tauri-apps/plugin-stronghold'; + * const location = new Location('Generic', { vault: 'my-vault', record: 'my-record' }); + * ``` + * + * @param type The location kind, either `Generic` or `Counter`. + * @param payload The data identifying the record inside the vault. + */ + constructor(type: string, payload: Record) { + this.type = type + this.payload = payload + } + + /** + * Creates a location addressing a record of a vault by its record path. + * + * @example + * ```typescript + * import { Location } from '@tauri-apps/plugin-stronghold'; + * const location = Location.generic('my-vault', 'my-record'); + * ``` + * + * @param vault The path of the vault holding the record. + * @param record The path of the record inside the vault. + * @returns The location of the record. + */ + static generic(vault: VaultPath, record: RecordPath): Location { + return new Location('Generic', { + vault, + record + }) + } + + /** + * Creates a location addressing a record of a vault by a counter. + * + * @example + * ```typescript + * import { Location } from '@tauri-apps/plugin-stronghold'; + * const location = Location.counter('my-vault', 0); + * ``` + * + * @param vault The path of the vault holding the record. + * @param counter The counter identifying the record inside the vault. + * @returns The location of the record. + */ + static counter(vault: VaultPath, counter: number): Location { + return new Location('Counter', { + vault, + counter + }) + } +} + +class ProcedureExecutor { + procedureArgs: Record + + constructor(procedureArgs: Record) { + this.procedureArgs = procedureArgs + } + + /** + * Generate a SLIP10 seed for the given location. + * @param outputLocation Location of the record where the seed will be stored. + * @param sizeBytes The size in bytes of the SLIP10 seed. + * @returns A promise resolving to the bytes returned by the procedure. + */ + async generateSLIP10Seed( + outputLocation: Location, + sizeBytes?: number + ): Promise { + return await invoke('plugin:stronghold|execute_procedure', { + ...this.procedureArgs, + procedure: { + type: 'SLIP10Generate', + payload: { + output: outputLocation, + sizeBytes + } + } + }).then((n) => Uint8Array.from(n)) + } + + /** + * Derive a SLIP10 private key using a seed or key. The derivation is always + * performed on the Ed25519 curve. + * @param chain The chain path. + * @param source The source type, either 'Seed' or 'Key'. + * @param sourceLocation The source location, must be the `outputLocation` of a previous call to `generateSLIP10Seed` or `deriveSLIP10`. + * @param outputLocation Location of the record where the private key will be stored. + * @returns A promise resolving to the bytes returned by the procedure. + */ + async deriveSLIP10( + chain: number[], + source: 'Seed' | 'Key', + sourceLocation: Location, + outputLocation: Location + ): Promise { + return await invoke('plugin:stronghold|execute_procedure', { + ...this.procedureArgs, + procedure: { + type: 'SLIP10Derive', + payload: { + chain, + input: { + type: source, + payload: sourceLocation + }, + output: outputLocation + } + } + }).then((n) => Uint8Array.from(n)) + } + + /** + * Store a BIP39 mnemonic. + * @param mnemonic The mnemonic string. + * @param outputLocation The location of the record where the BIP39 mnemonic will be stored. + * @param passphrase The optional mnemonic passphrase. + * @returns A promise resolving to the bytes returned by the procedure. + */ + async recoverBIP39( + mnemonic: string, + outputLocation: Location, + passphrase?: string + ): Promise { + return await invoke('plugin:stronghold|execute_procedure', { + ...this.procedureArgs, + procedure: { + type: 'BIP39Recover', + payload: { + mnemonic, + passphrase, + output: outputLocation + } + } + }).then((n) => Uint8Array.from(n)) + } + + /** + * Generate a BIP39 seed. The mnemonic is generated in English. + * @param outputLocation The location of the record where the BIP39 seed will be stored. + * @param passphrase The optional mnemonic passphrase. + * @returns A promise resolving to the bytes returned by the procedure. + */ + async generateBIP39( + outputLocation: Location, + passphrase?: string + ): Promise { + return await invoke('plugin:stronghold|execute_procedure', { + ...this.procedureArgs, + procedure: { + type: 'BIP39Generate', + payload: { + output: outputLocation, + passphrase + } + } + }).then((n) => Uint8Array.from(n)) + } + + /** + * Gets the Ed25519 public key of a SLIP10 private key. + * @param privateKeyLocation The location of the private key. Must be the `outputLocation` of a previous call to `deriveSLIP10`. + * @returns A promise resolving to the public key hex string. + * + * @since 2.0.0 + */ + async getEd25519PublicKey(privateKeyLocation: Location): Promise { + return await invoke('plugin:stronghold|execute_procedure', { + ...this.procedureArgs, + procedure: { + type: 'PublicKey', + payload: { + type: 'Ed25519', + privateKey: privateKeyLocation + } + } + }).then((n) => Uint8Array.from(n)) + } + + /** + * Creates a Ed25519 signature from a private key. + * @param privateKeyLocation The location of the record where the private key is stored. Must be the `outputLocation` of a previous call to `deriveSLIP10`. + * @param msg The message to sign. + * @returns A promise resolving to the signature hex string. + * + * @since 2.0.0 + */ + async signEd25519( + privateKeyLocation: Location, + msg: string + ): Promise { + return await invoke('plugin:stronghold|execute_procedure', { + ...this.procedureArgs, + procedure: { + type: 'Ed25519Sign', + payload: { + privateKey: privateKeyLocation, + msg + } + } + }).then((n) => Uint8Array.from(n)) + } +} + +/** + * A client of a stronghold snapshot, owning a set of vaults and a key-value store. + * Clients are obtained with {@link Stronghold.loadClient} and {@link Stronghold.createClient}. + * + * @since 2.0.0 + */ +export class Client { + /** The path of the snapshot file this client belongs to. */ + path: string + /** The name identifying this client inside the snapshot. */ + name: ClientPath + + /** + * Creates a client handle for a client that was already loaded or created. + * Prefer {@link Stronghold.loadClient} and {@link Stronghold.createClient}, + * which also register the client on the Rust side. + * + * @example + * ```typescript + * import { Client } from '@tauri-apps/plugin-stronghold'; + * const client = new Client('/path/to/snapshot.hold', 'my-client'); + * ``` + * + * @param path The path of the snapshot file the client belongs to. + * @param name The name identifying the client inside the snapshot. + */ + constructor(path: string, name: ClientPath) { + this.path = path + this.name = name + } + + /** + * Gets a handle to the vault with the given name. The vault is created on the + * Rust side when the first secret is written to it. + * + * @example + * ```typescript + * import { Stronghold } from '@tauri-apps/plugin-stronghold'; + * const stronghold = await Stronghold.load('/path/to/snapshot.hold', 'password'); + * const client = await stronghold.createClient('my-client'); + * const vault = client.getVault('my-vault'); + * ``` + * + * @param name The path of the vault. + * @returns The vault handle. + */ + getVault(name: VaultPath): Vault { + return new Vault(this.path, this.name, name) + } + + /** + * Gets a handle to the key-value store of this client. + * + * @example + * ```typescript + * import { Stronghold } from '@tauri-apps/plugin-stronghold'; + * const stronghold = await Stronghold.load('/path/to/snapshot.hold', 'password'); + * const client = await stronghold.createClient('my-client'); + * const store = client.getStore(); + * ``` + * + * @returns The store handle. + */ + getStore(): Store { + return new Store(this.path, this.name) + } +} + +/** + * The key-value store of a {@link Client}. Unlike a {@link Vault}, the values + * stored here can be read back directly. + * + * @since 2.0.0 + */ +export class Store { + /** The path of the snapshot file this store belongs to. */ + path: string + /** The name of the client owning this store. */ + client: ClientPath + + /** + * Creates a store handle for a client that was already loaded or created. + * Prefer {@link Client.getStore}. + * + * @example + * ```typescript + * import { Store } from '@tauri-apps/plugin-stronghold'; + * const store = new Store('/path/to/snapshot.hold', 'my-client'); + * ``` + * + * @param path The path of the snapshot file the store belongs to. + * @param client The name of the client owning the store. + */ + constructor(path: string, client: ClientPath) { + this.path = path + this.client = client + } + + /** + * Reads the value of a record of this store. + * + * @example + * ```typescript + * import { Stronghold } from '@tauri-apps/plugin-stronghold'; + * const stronghold = await Stronghold.load('/path/to/snapshot.hold', 'password'); + * const client = await stronghold.createClient('my-client'); + * const value = await client.getStore().get('my-key'); + * ``` + * + * @param key The key of the record. + * @returns A promise resolving to the stored value, or `null` if the key does not exist. + */ + async get(key: StoreKey): Promise { + return await invoke('plugin:stronghold|get_store_record', { + snapshotPath: this.path, + client: this.client, + key + }).then((v) => v && Uint8Array.from(v)) + } + + /** + * Inserts a record in this store, replacing the previous value of the key. + * Note that the snapshot is only persisted when {@link Stronghold.save} is called. + * + * @example + * ```typescript + * import { Stronghold } from '@tauri-apps/plugin-stronghold'; + * const stronghold = await Stronghold.load('/path/to/snapshot.hold', 'password'); + * const client = await stronghold.createClient('my-client'); + * const data = Array.from(new TextEncoder().encode('Hello, World!')); + * await client.getStore().insert('my-key', data); + * await stronghold.save(); + * ``` + * + * @param key The key of the record. + * @param value The value of the record, as an array of bytes. + * @param lifetime The optional duration after which the record expires. + */ + async insert( + key: StoreKey, + value: number[], + lifetime?: Duration + ): Promise { + await invoke('plugin:stronghold|save_store_record', { + snapshotPath: this.path, + client: this.client, + key, + value, + lifetime + }) + } + + /** + * Deletes a record from this store. + * + * @example + * ```typescript + * import { Stronghold } from '@tauri-apps/plugin-stronghold'; + * const stronghold = await Stronghold.load('/path/to/snapshot.hold', 'password'); + * const client = await stronghold.createClient('my-client'); + * await client.getStore().remove('my-key'); + * ``` + * + * @param key The key of the record. + * @returns A promise resolving to the deleted value, or `null` if the key did not exist. + */ + async remove(key: StoreKey): Promise { + return await invoke( + 'plugin:stronghold|remove_store_record', + { + snapshotPath: this.path, + client: this.client, + key + } + ).then((v) => v && Uint8Array.from(v)) + } +} + +/** + * A key-value storage that allows create, update and delete operations. + * It does not allow reading the data, so one of the procedures must be used to manipulate + * the stored data, allowing secure storage of secrets. + * + * @since 2.0.0 + */ +export class Vault extends ProcedureExecutor { + /** The path of the snapshot file this vault belongs to. */ + path: string + /** The name of the client owning this vault. */ + client: ClientPath + /** The path identifying this vault inside the client. */ + name: VaultPath + + /** + * Creates a vault handle for a client that was already loaded or created. + * Prefer {@link Client.getVault}. + * + * @example + * ```typescript + * import { Vault } from '@tauri-apps/plugin-stronghold'; + * const vault = new Vault('/path/to/snapshot.hold', 'my-client', 'my-vault'); + * ``` + * + * @param path The path of the snapshot file the vault belongs to. + * @param client The name of the client owning the vault. + * @param name The path identifying the vault inside the client. + */ + constructor(path: string, client: ClientPath, name: VaultPath) { + super({ + snapshotPath: path, + client, + vault: name + }) + this.path = path + this.client = client + this.name = name + } + + /** + * Writes a secret to this vault. Note that the snapshot is only persisted + * when {@link Stronghold.save} is called. + * + * @example + * ```typescript + * import { Stronghold } from '@tauri-apps/plugin-stronghold'; + * const stronghold = await Stronghold.load('/path/to/snapshot.hold', 'password'); + * const client = await stronghold.createClient('my-client'); + * const secret = Array.from(new TextEncoder().encode('secret value')); + * await client.getVault('my-vault').insert('my-record', secret); + * await stronghold.save(); + * ``` + * + * @param recordPath The path of the record inside this vault. + * @param secret The secret to store, as an array of bytes. + */ + async insert(recordPath: RecordPath, secret: number[]): Promise { + await invoke('plugin:stronghold|save_secret', { + snapshotPath: this.path, + client: this.client, + vault: this.name, + recordPath, + secret + }) + } + + /** + * Deletes a secret from this vault. Only the record path of the given + * location is used, the vault is always this one. + * + * @example + * ```typescript + * import { Stronghold, Location } from '@tauri-apps/plugin-stronghold'; + * const stronghold = await Stronghold.load('/path/to/snapshot.hold', 'password'); + * const client = await stronghold.createClient('my-client'); + * await client + * .getVault('my-vault') + * .remove(Location.generic('my-vault', 'my-record')); + * ``` + * + * @param location The location of the record to delete. + */ + async remove(location: Location): Promise { + await invoke('plugin:stronghold|remove_secret', { + snapshotPath: this.path, + client: this.client, + vault: this.name, + recordPath: location.payload.record + }) + } +} + +/** + * A representation of an access to a stronghold. + * + * @since 2.0.0 + */ +export class Stronghold { + /** The path of the snapshot file backing this stronghold. */ + path: string + + /** + * Creates a handle to the stronghold initialized for the given snapshot path. + * @param path The path of the snapshot file. + */ + private constructor(path: string) { + this.path = path + } + + /** + * Load the snapshot if it exists (password must match), or start a fresh stronghold instance otherwise. + * + * @example + * ```typescript + * import { Stronghold } from '@tauri-apps/plugin-stronghold'; + * import { appDataDir } from '@tauri-apps/api/path'; + * const stronghold = await Stronghold.load(`${await appDataDir()}/vault.hold`, 'password'); + * ``` + * + * @param path The path of the snapshot file. + * @param password The password used to encrypt and decrypt the snapshot. + * @returns A promise resolving to the stronghold instance. + */ + static async load(path: string, password: string): Promise { + return await invoke('plugin:stronghold|initialize', { + snapshotPath: path, + password + }).then(() => new Stronghold(path)) + } + + /** + * Saves the snapshot and removes this instance from the cache. + * + * @example + * ```typescript + * import { Stronghold } from '@tauri-apps/plugin-stronghold'; + * const stronghold = await Stronghold.load('/path/to/snapshot.hold', 'password'); + * await stronghold.unload(); + * ``` + */ + async unload(): Promise { + await invoke('plugin:stronghold|destroy', { + snapshotPath: this.path + }) + } + + /** + * Loads an existing client from the snapshot. The promise rejects if the + * client does not exist in the snapshot or was already loaded. + * + * @example + * ```typescript + * import { Stronghold } from '@tauri-apps/plugin-stronghold'; + * const stronghold = await Stronghold.load('/path/to/snapshot.hold', 'password'); + * const client = await stronghold.loadClient('my-client'); + * ``` + * + * @param client The name of the client. + * @returns A promise resolving to the loaded client. + */ + async loadClient(client: ClientPath): Promise { + return await invoke('plugin:stronghold|load_client', { + snapshotPath: this.path, + client + }).then(() => new Client(this.path, client)) + } + + /** + * Creates a new empty client on this stronghold. + * + * @example + * ```typescript + * import { Stronghold } from '@tauri-apps/plugin-stronghold'; + * const stronghold = await Stronghold.load('/path/to/snapshot.hold', 'password'); + * let client; + * try { + * client = await stronghold.loadClient('my-client'); + * } catch { + * client = await stronghold.createClient('my-client'); + * } + * ``` + * + * @param client The name of the client. + * @returns A promise resolving to the created client. + */ + async createClient(client: ClientPath): Promise { + return await invoke('plugin:stronghold|create_client', { + snapshotPath: this.path, + client + }).then(() => new Client(this.path, client)) + } + + /** + * Persists the stronghold state to the snapshot. + * + * @example + * ```typescript + * import { Stronghold } from '@tauri-apps/plugin-stronghold'; + * const stronghold = await Stronghold.load('/path/to/snapshot.hold', 'password'); + * const client = await stronghold.createClient('my-client'); + * await client.getStore().insert('my-key', [1, 2, 3]); + * await stronghold.save(); + * ``` + */ + async save(): Promise { + await invoke('plugin:stronghold|save', { + snapshotPath: this.path + }) + } +} diff --git a/plugins/stronghold/package.json b/plugins/stronghold/package.json new file mode 100644 index 0000000..446254b --- /dev/null +++ b/plugins/stronghold/package.json @@ -0,0 +1,30 @@ +{ + "name": "@tauri-apps/plugin-stronghold", + "version": "2.3.2", + "description": "Store secrets and keys using the IOTA Stronghold encrypted database.", + "license": "MIT OR Apache-2.0", + "authors": [ + "Tauri Programme within The Commons Conservancy" + ], + "repository": "https://github.com/tauri-apps/plugins-workspace", + "type": "module", + "types": "./dist-js/index.d.ts", + "main": "./dist-js/index.cjs", + "module": "./dist-js/index.js", + "exports": { + "types": "./dist-js/index.d.ts", + "import": "./dist-js/index.js", + "require": "./dist-js/index.cjs" + }, + "scripts": { + "build": "rollup -c" + }, + "files": [ + "dist-js", + "README.md", + "LICENSE" + ], + "dependencies": { + "@tauri-apps/api": "^2.11.0" + } +} diff --git a/plugins/stronghold/permissions/autogenerated/commands/create_client.toml b/plugins/stronghold/permissions/autogenerated/commands/create_client.toml new file mode 100644 index 0000000..1405d1e --- /dev/null +++ b/plugins/stronghold/permissions/autogenerated/commands/create_client.toml @@ -0,0 +1,13 @@ +# Automatically generated - DO NOT EDIT! + +"$schema" = "../../schemas/schema.json" + +[[permission]] +identifier = "allow-create-client" +description = "Enables the create_client command without any pre-configured scope." +commands.allow = ["create_client"] + +[[permission]] +identifier = "deny-create-client" +description = "Denies the create_client command without any pre-configured scope." +commands.deny = ["create_client"] diff --git a/plugins/stronghold/permissions/autogenerated/commands/destroy.toml b/plugins/stronghold/permissions/autogenerated/commands/destroy.toml new file mode 100644 index 0000000..7958061 --- /dev/null +++ b/plugins/stronghold/permissions/autogenerated/commands/destroy.toml @@ -0,0 +1,13 @@ +# Automatically generated - DO NOT EDIT! + +"$schema" = "../../schemas/schema.json" + +[[permission]] +identifier = "allow-destroy" +description = "Enables the destroy command without any pre-configured scope." +commands.allow = ["destroy"] + +[[permission]] +identifier = "deny-destroy" +description = "Denies the destroy command without any pre-configured scope." +commands.deny = ["destroy"] diff --git a/plugins/stronghold/permissions/autogenerated/commands/execute_procedure.toml b/plugins/stronghold/permissions/autogenerated/commands/execute_procedure.toml new file mode 100644 index 0000000..f6c2d2b --- /dev/null +++ b/plugins/stronghold/permissions/autogenerated/commands/execute_procedure.toml @@ -0,0 +1,13 @@ +# Automatically generated - DO NOT EDIT! + +"$schema" = "../../schemas/schema.json" + +[[permission]] +identifier = "allow-execute-procedure" +description = "Enables the execute_procedure command without any pre-configured scope." +commands.allow = ["execute_procedure"] + +[[permission]] +identifier = "deny-execute-procedure" +description = "Denies the execute_procedure command without any pre-configured scope." +commands.deny = ["execute_procedure"] diff --git a/plugins/stronghold/permissions/autogenerated/commands/get_store_record.toml b/plugins/stronghold/permissions/autogenerated/commands/get_store_record.toml new file mode 100644 index 0000000..84a9e53 --- /dev/null +++ b/plugins/stronghold/permissions/autogenerated/commands/get_store_record.toml @@ -0,0 +1,13 @@ +# Automatically generated - DO NOT EDIT! + +"$schema" = "../../schemas/schema.json" + +[[permission]] +identifier = "allow-get-store-record" +description = "Enables the get_store_record command without any pre-configured scope." +commands.allow = ["get_store_record"] + +[[permission]] +identifier = "deny-get-store-record" +description = "Denies the get_store_record command without any pre-configured scope." +commands.deny = ["get_store_record"] diff --git a/plugins/stronghold/permissions/autogenerated/commands/initialize.toml b/plugins/stronghold/permissions/autogenerated/commands/initialize.toml new file mode 100644 index 0000000..73961d1 --- /dev/null +++ b/plugins/stronghold/permissions/autogenerated/commands/initialize.toml @@ -0,0 +1,13 @@ +# Automatically generated - DO NOT EDIT! + +"$schema" = "../../schemas/schema.json" + +[[permission]] +identifier = "allow-initialize" +description = "Enables the initialize command without any pre-configured scope." +commands.allow = ["initialize"] + +[[permission]] +identifier = "deny-initialize" +description = "Denies the initialize command without any pre-configured scope." +commands.deny = ["initialize"] diff --git a/plugins/stronghold/permissions/autogenerated/commands/load_client.toml b/plugins/stronghold/permissions/autogenerated/commands/load_client.toml new file mode 100644 index 0000000..77949c1 --- /dev/null +++ b/plugins/stronghold/permissions/autogenerated/commands/load_client.toml @@ -0,0 +1,13 @@ +# Automatically generated - DO NOT EDIT! + +"$schema" = "../../schemas/schema.json" + +[[permission]] +identifier = "allow-load-client" +description = "Enables the load_client command without any pre-configured scope." +commands.allow = ["load_client"] + +[[permission]] +identifier = "deny-load-client" +description = "Denies the load_client command without any pre-configured scope." +commands.deny = ["load_client"] diff --git a/plugins/stronghold/permissions/autogenerated/commands/remove_secret.toml b/plugins/stronghold/permissions/autogenerated/commands/remove_secret.toml new file mode 100644 index 0000000..6dafb68 --- /dev/null +++ b/plugins/stronghold/permissions/autogenerated/commands/remove_secret.toml @@ -0,0 +1,13 @@ +# Automatically generated - DO NOT EDIT! + +"$schema" = "../../schemas/schema.json" + +[[permission]] +identifier = "allow-remove-secret" +description = "Enables the remove_secret command without any pre-configured scope." +commands.allow = ["remove_secret"] + +[[permission]] +identifier = "deny-remove-secret" +description = "Denies the remove_secret command without any pre-configured scope." +commands.deny = ["remove_secret"] diff --git a/plugins/stronghold/permissions/autogenerated/commands/remove_store_record.toml b/plugins/stronghold/permissions/autogenerated/commands/remove_store_record.toml new file mode 100644 index 0000000..20fd1e8 --- /dev/null +++ b/plugins/stronghold/permissions/autogenerated/commands/remove_store_record.toml @@ -0,0 +1,13 @@ +# Automatically generated - DO NOT EDIT! + +"$schema" = "../../schemas/schema.json" + +[[permission]] +identifier = "allow-remove-store-record" +description = "Enables the remove_store_record command without any pre-configured scope." +commands.allow = ["remove_store_record"] + +[[permission]] +identifier = "deny-remove-store-record" +description = "Denies the remove_store_record command without any pre-configured scope." +commands.deny = ["remove_store_record"] diff --git a/plugins/stronghold/permissions/autogenerated/commands/save.toml b/plugins/stronghold/permissions/autogenerated/commands/save.toml new file mode 100644 index 0000000..d3e8422 --- /dev/null +++ b/plugins/stronghold/permissions/autogenerated/commands/save.toml @@ -0,0 +1,13 @@ +# Automatically generated - DO NOT EDIT! + +"$schema" = "../../schemas/schema.json" + +[[permission]] +identifier = "allow-save" +description = "Enables the save command without any pre-configured scope." +commands.allow = ["save"] + +[[permission]] +identifier = "deny-save" +description = "Denies the save command without any pre-configured scope." +commands.deny = ["save"] diff --git a/plugins/stronghold/permissions/autogenerated/commands/save_secret.toml b/plugins/stronghold/permissions/autogenerated/commands/save_secret.toml new file mode 100644 index 0000000..d999f0b --- /dev/null +++ b/plugins/stronghold/permissions/autogenerated/commands/save_secret.toml @@ -0,0 +1,13 @@ +# Automatically generated - DO NOT EDIT! + +"$schema" = "../../schemas/schema.json" + +[[permission]] +identifier = "allow-save-secret" +description = "Enables the save_secret command without any pre-configured scope." +commands.allow = ["save_secret"] + +[[permission]] +identifier = "deny-save-secret" +description = "Denies the save_secret command without any pre-configured scope." +commands.deny = ["save_secret"] diff --git a/plugins/stronghold/permissions/autogenerated/commands/save_store_record.toml b/plugins/stronghold/permissions/autogenerated/commands/save_store_record.toml new file mode 100644 index 0000000..8292ded --- /dev/null +++ b/plugins/stronghold/permissions/autogenerated/commands/save_store_record.toml @@ -0,0 +1,13 @@ +# Automatically generated - DO NOT EDIT! + +"$schema" = "../../schemas/schema.json" + +[[permission]] +identifier = "allow-save-store-record" +description = "Enables the save_store_record command without any pre-configured scope." +commands.allow = ["save_store_record"] + +[[permission]] +identifier = "deny-save-store-record" +description = "Denies the save_store_record command without any pre-configured scope." +commands.deny = ["save_store_record"] diff --git a/plugins/stronghold/permissions/autogenerated/reference.md b/plugins/stronghold/permissions/autogenerated/reference.md new file mode 100644 index 0000000..e660b42 --- /dev/null +++ b/plugins/stronghold/permissions/autogenerated/reference.md @@ -0,0 +1,315 @@ +## Default Permission + +This permission set configures what kind of +operations are available from the stronghold plugin. + +#### Granted Permissions + +All non-destructive operations are enabled by default. + +#### This default permission set includes the following: + +- `allow-create-client` +- `allow-get-store-record` +- `allow-initialize` +- `allow-execute-procedure` +- `allow-load-client` +- `allow-save-secret` +- `allow-save-store-record` +- `allow-save` + +## Permission Table + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
IdentifierDescription
+ +`stronghold:allow-create-client` + + + +Enables the create_client command without any pre-configured scope. + +
+ +`stronghold:deny-create-client` + + + +Denies the create_client command without any pre-configured scope. + +
+ +`stronghold:allow-destroy` + + + +Enables the destroy command without any pre-configured scope. + +
+ +`stronghold:deny-destroy` + + + +Denies the destroy command without any pre-configured scope. + +
+ +`stronghold:allow-execute-procedure` + + + +Enables the execute_procedure command without any pre-configured scope. + +
+ +`stronghold:deny-execute-procedure` + + + +Denies the execute_procedure command without any pre-configured scope. + +
+ +`stronghold:allow-get-store-record` + + + +Enables the get_store_record command without any pre-configured scope. + +
+ +`stronghold:deny-get-store-record` + + + +Denies the get_store_record command without any pre-configured scope. + +
+ +`stronghold:allow-initialize` + + + +Enables the initialize command without any pre-configured scope. + +
+ +`stronghold:deny-initialize` + + + +Denies the initialize command without any pre-configured scope. + +
+ +`stronghold:allow-load-client` + + + +Enables the load_client command without any pre-configured scope. + +
+ +`stronghold:deny-load-client` + + + +Denies the load_client command without any pre-configured scope. + +
+ +`stronghold:allow-remove-secret` + + + +Enables the remove_secret command without any pre-configured scope. + +
+ +`stronghold:deny-remove-secret` + + + +Denies the remove_secret command without any pre-configured scope. + +
+ +`stronghold:allow-remove-store-record` + + + +Enables the remove_store_record command without any pre-configured scope. + +
+ +`stronghold:deny-remove-store-record` + + + +Denies the remove_store_record command without any pre-configured scope. + +
+ +`stronghold:allow-save` + + + +Enables the save command without any pre-configured scope. + +
+ +`stronghold:deny-save` + + + +Denies the save command without any pre-configured scope. + +
+ +`stronghold:allow-save-secret` + + + +Enables the save_secret command without any pre-configured scope. + +
+ +`stronghold:deny-save-secret` + + + +Denies the save_secret command without any pre-configured scope. + +
+ +`stronghold:allow-save-store-record` + + + +Enables the save_store_record command without any pre-configured scope. + +
+ +`stronghold:deny-save-store-record` + + + +Denies the save_store_record command without any pre-configured scope. + +
diff --git a/plugins/stronghold/permissions/default.toml b/plugins/stronghold/permissions/default.toml new file mode 100644 index 0000000..c157fe7 --- /dev/null +++ b/plugins/stronghold/permissions/default.toml @@ -0,0 +1,22 @@ +"$schema" = "schemas/schema.json" + +[default] +description = """ +This permission set configures what kind of +operations are available from the stronghold plugin. + +#### Granted Permissions + +All non-destructive operations are enabled by default. + +""" +permissions = [ + "allow-create-client", + "allow-get-store-record", + "allow-initialize", + "allow-execute-procedure", + "allow-load-client", + "allow-save-secret", + "allow-save-store-record", + "allow-save", +] diff --git a/plugins/stronghold/permissions/schemas/schema.json b/plugins/stronghold/permissions/schemas/schema.json new file mode 100644 index 0000000..6af87c6 --- /dev/null +++ b/plugins/stronghold/permissions/schemas/schema.json @@ -0,0 +1,438 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "PermissionFile", + "description": "Permission file that can define a default permission, a set of permissions or a list of inlined permissions.", + "type": "object", + "properties": { + "default": { + "description": "The default permission set for the plugin", + "anyOf": [ + { + "$ref": "#/definitions/DefaultPermission" + }, + { + "type": "null" + } + ] + }, + "set": { + "description": "A list of permissions sets defined", + "type": "array", + "items": { + "$ref": "#/definitions/PermissionSet" + } + }, + "permission": { + "description": "A list of inlined permissions", + "default": [], + "type": "array", + "items": { + "$ref": "#/definitions/Permission" + } + } + }, + "definitions": { + "DefaultPermission": { + "description": "The default permission set of the plugin.\n\nWorks similarly to a permission with the \"default\" identifier.", + "type": "object", + "required": [ + "permissions" + ], + "properties": { + "version": { + "description": "The version of the permission.", + "type": [ + "integer", + "null" + ], + "format": "uint64", + "minimum": 1.0 + }, + "description": { + "description": "Human-readable description of what the permission does. Tauri convention is to use `

` headings in markdown content for Tauri documentation generation purposes.", + "type": [ + "string", + "null" + ] + }, + "permissions": { + "description": "All permissions this set contains.", + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "PermissionSet": { + "description": "A set of direct permissions grouped together under a new name.", + "type": "object", + "required": [ + "description", + "identifier", + "permissions" + ], + "properties": { + "identifier": { + "description": "A unique identifier for the permission.", + "type": "string" + }, + "description": { + "description": "Human-readable description of what the permission does.", + "type": "string" + }, + "permissions": { + "description": "All permissions this set contains.", + "type": "array", + "items": { + "$ref": "#/definitions/PermissionKind" + } + } + } + }, + "Permission": { + "description": "Descriptions of explicit privileges of commands.\n\nIt can enable commands to be accessible in the frontend of the application.\n\nIf the scope is defined it can be used to fine grain control the access of individual or multiple commands.", + "type": "object", + "required": [ + "identifier" + ], + "properties": { + "version": { + "description": "The version of the permission.", + "type": [ + "integer", + "null" + ], + "format": "uint64", + "minimum": 1.0 + }, + "identifier": { + "description": "A unique identifier for the permission.", + "type": "string" + }, + "description": { + "description": "Human-readable description of what the permission does. Tauri internal convention is to use `

` headings in markdown content for Tauri documentation generation purposes.", + "type": [ + "string", + "null" + ] + }, + "commands": { + "description": "Allowed or denied commands when using this permission.", + "default": { + "allow": [], + "deny": [] + }, + "allOf": [ + { + "$ref": "#/definitions/Commands" + } + ] + }, + "scope": { + "description": "Allowed or denied scoped when using this permission.", + "allOf": [ + { + "$ref": "#/definitions/Scopes" + } + ] + }, + "platforms": { + "description": "Target platforms this permission applies. By default all platforms are affected by this permission.", + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/Target" + } + } + } + }, + "Commands": { + "description": "Allowed and denied commands inside a permission.\n\nIf two commands clash inside of `allow` and `deny`, it should be denied by default.", + "type": "object", + "properties": { + "allow": { + "description": "Allowed command.", + "default": [], + "type": "array", + "items": { + "type": "string" + } + }, + "deny": { + "description": "Denied command, which takes priority.", + "default": [], + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "Scopes": { + "description": "An argument for fine grained behavior control of Tauri commands.\n\nIt can be of any serde serializable type and is used to allow or prevent certain actions inside a Tauri command. The configured scope is passed to the command and will be enforced by the command implementation.\n\n## Example\n\n```json { \"allow\": [{ \"path\": \"$HOME/**\" }], \"deny\": [{ \"path\": \"$HOME/secret.txt\" }] } ```", + "type": "object", + "properties": { + "allow": { + "description": "Data that defines what is allowed by the scope.", + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/Value" + } + }, + "deny": { + "description": "Data that defines what is denied by the scope. This should be prioritized by validation logic.", + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/Value" + } + } + } + }, + "Value": { + "description": "All supported ACL values.", + "anyOf": [ + { + "description": "Represents a null JSON value.", + "type": "null" + }, + { + "description": "Represents a [`bool`].", + "type": "boolean" + }, + { + "description": "Represents a valid ACL [`Number`].", + "allOf": [ + { + "$ref": "#/definitions/Number" + } + ] + }, + { + "description": "Represents a [`String`].", + "type": "string" + }, + { + "description": "Represents a list of other [`Value`]s.", + "type": "array", + "items": { + "$ref": "#/definitions/Value" + } + }, + { + "description": "Represents a map of [`String`] keys to [`Value`]s.", + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/Value" + } + } + ] + }, + "Number": { + "description": "A valid ACL number.", + "anyOf": [ + { + "description": "Represents an [`i64`].", + "type": "integer", + "format": "int64" + }, + { + "description": "Represents a [`f64`].", + "type": "number", + "format": "double" + } + ] + }, + "Target": { + "description": "Platform target.", + "oneOf": [ + { + "description": "MacOS.", + "type": "string", + "enum": [ + "macOS" + ] + }, + { + "description": "Windows.", + "type": "string", + "enum": [ + "windows" + ] + }, + { + "description": "Linux.", + "type": "string", + "enum": [ + "linux" + ] + }, + { + "description": "Android.", + "type": "string", + "enum": [ + "android" + ] + }, + { + "description": "iOS.", + "type": "string", + "enum": [ + "iOS" + ] + } + ] + }, + "PermissionKind": { + "type": "string", + "oneOf": [ + { + "description": "Enables the create_client command without any pre-configured scope.", + "type": "string", + "const": "allow-create-client", + "markdownDescription": "Enables the create_client command without any pre-configured scope." + }, + { + "description": "Denies the create_client command without any pre-configured scope.", + "type": "string", + "const": "deny-create-client", + "markdownDescription": "Denies the create_client command without any pre-configured scope." + }, + { + "description": "Enables the destroy command without any pre-configured scope.", + "type": "string", + "const": "allow-destroy", + "markdownDescription": "Enables the destroy command without any pre-configured scope." + }, + { + "description": "Denies the destroy command without any pre-configured scope.", + "type": "string", + "const": "deny-destroy", + "markdownDescription": "Denies the destroy command without any pre-configured scope." + }, + { + "description": "Enables the execute_procedure command without any pre-configured scope.", + "type": "string", + "const": "allow-execute-procedure", + "markdownDescription": "Enables the execute_procedure command without any pre-configured scope." + }, + { + "description": "Denies the execute_procedure command without any pre-configured scope.", + "type": "string", + "const": "deny-execute-procedure", + "markdownDescription": "Denies the execute_procedure command without any pre-configured scope." + }, + { + "description": "Enables the get_store_record command without any pre-configured scope.", + "type": "string", + "const": "allow-get-store-record", + "markdownDescription": "Enables the get_store_record command without any pre-configured scope." + }, + { + "description": "Denies the get_store_record command without any pre-configured scope.", + "type": "string", + "const": "deny-get-store-record", + "markdownDescription": "Denies the get_store_record command without any pre-configured scope." + }, + { + "description": "Enables the initialize command without any pre-configured scope.", + "type": "string", + "const": "allow-initialize", + "markdownDescription": "Enables the initialize command without any pre-configured scope." + }, + { + "description": "Denies the initialize command without any pre-configured scope.", + "type": "string", + "const": "deny-initialize", + "markdownDescription": "Denies the initialize command without any pre-configured scope." + }, + { + "description": "Enables the load_client command without any pre-configured scope.", + "type": "string", + "const": "allow-load-client", + "markdownDescription": "Enables the load_client command without any pre-configured scope." + }, + { + "description": "Denies the load_client command without any pre-configured scope.", + "type": "string", + "const": "deny-load-client", + "markdownDescription": "Denies the load_client command without any pre-configured scope." + }, + { + "description": "Enables the remove_secret command without any pre-configured scope.", + "type": "string", + "const": "allow-remove-secret", + "markdownDescription": "Enables the remove_secret command without any pre-configured scope." + }, + { + "description": "Denies the remove_secret command without any pre-configured scope.", + "type": "string", + "const": "deny-remove-secret", + "markdownDescription": "Denies the remove_secret command without any pre-configured scope." + }, + { + "description": "Enables the remove_store_record command without any pre-configured scope.", + "type": "string", + "const": "allow-remove-store-record", + "markdownDescription": "Enables the remove_store_record command without any pre-configured scope." + }, + { + "description": "Denies the remove_store_record command without any pre-configured scope.", + "type": "string", + "const": "deny-remove-store-record", + "markdownDescription": "Denies the remove_store_record command without any pre-configured scope." + }, + { + "description": "Enables the save command without any pre-configured scope.", + "type": "string", + "const": "allow-save", + "markdownDescription": "Enables the save command without any pre-configured scope." + }, + { + "description": "Denies the save command without any pre-configured scope.", + "type": "string", + "const": "deny-save", + "markdownDescription": "Denies the save command without any pre-configured scope." + }, + { + "description": "Enables the save_secret command without any pre-configured scope.", + "type": "string", + "const": "allow-save-secret", + "markdownDescription": "Enables the save_secret command without any pre-configured scope." + }, + { + "description": "Denies the save_secret command without any pre-configured scope.", + "type": "string", + "const": "deny-save-secret", + "markdownDescription": "Denies the save_secret command without any pre-configured scope." + }, + { + "description": "Enables the save_store_record command without any pre-configured scope.", + "type": "string", + "const": "allow-save-store-record", + "markdownDescription": "Enables the save_store_record command without any pre-configured scope." + }, + { + "description": "Denies the save_store_record command without any pre-configured scope.", + "type": "string", + "const": "deny-save-store-record", + "markdownDescription": "Denies the save_store_record command without any pre-configured scope." + }, + { + "description": "This permission set configures what kind of\noperations are available from the stronghold plugin.\n\n#### Granted Permissions\n\nAll non-destructive operations are enabled by default.\n\n\n#### This default permission set includes:\n\n- `allow-create-client`\n- `allow-get-store-record`\n- `allow-initialize`\n- `allow-execute-procedure`\n- `allow-load-client`\n- `allow-save-secret`\n- `allow-save-store-record`\n- `allow-save`", + "type": "string", + "const": "default", + "markdownDescription": "This permission set configures what kind of\noperations are available from the stronghold plugin.\n\n#### Granted Permissions\n\nAll non-destructive operations are enabled by default.\n\n\n#### This default permission set includes:\n\n- `allow-create-client`\n- `allow-get-store-record`\n- `allow-initialize`\n- `allow-execute-procedure`\n- `allow-load-client`\n- `allow-save-secret`\n- `allow-save-store-record`\n- `allow-save`" + } + ] + } + } +} \ No newline at end of file diff --git a/plugins/stronghold/rollup.config.js b/plugins/stronghold/rollup.config.js new file mode 100644 index 0000000..1f349ec --- /dev/null +++ b/plugins/stronghold/rollup.config.js @@ -0,0 +1,7 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +import { createConfig } from '../../shared/rollup.config.js' + +export default createConfig() diff --git a/plugins/stronghold/src/kdf.rs b/plugins/stronghold/src/kdf.rs new file mode 100644 index 0000000..f0d3aa8 --- /dev/null +++ b/plugins/stronghold/src/kdf.rs @@ -0,0 +1,52 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +//! Key derivation used to turn the user password into the key that encrypts a snapshot. +//! +//! Only available when the **kdf** Cargo feature is enabled, which is the case by default. + +use rand_chacha::ChaCha20Rng; +use rand_core::{RngCore, SeedableRng}; +use std::path::Path; + +/// NOTE: Hash supplied to Stronghold must be 32bits long. +/// This is a current limitation of Stronghold. +const HASH_LENGTH: usize = 32; + +/// Password hashing functions that can be used as the key derivation function of +/// [`Builder::new`](crate::Builder::new). +pub struct KeyDerivation {} + +impl KeyDerivation { + /// Hashes `password` with Argon2 using the salt stored in `salt_path`, returning the + /// 32 bytes key used to encrypt a snapshot. + /// + /// The salt is read from `salt_path` when that file already exists, otherwise a new + /// random salt is generated and written to it. + /// + /// # Panics + /// + /// Panics when the salt file cannot be read or written, when its contents are not + /// 32 bytes long, or when hashing the password fails. + pub fn argon2(password: &str, salt_path: &Path) -> Vec { + let mut salt = [0u8; HASH_LENGTH]; + create_or_get_salt(&mut salt, salt_path); + + argon2::hash_raw(password.as_bytes(), &salt, &Default::default()) + .expect("Failed to generate hash for password") + } +} + +fn create_or_get_salt(salt: &mut [u8], salt_path: &Path) { + if salt_path.is_file() { + // Get existing salt + let tmp = std::fs::read(salt_path).unwrap(); + salt.clone_from_slice(&tmp); + } else { + // Generate new salt + let mut gen = ChaCha20Rng::from_os_rng(); + gen.fill_bytes(salt); + std::fs::write(salt_path, salt).expect("Failed to write salt for Stronghold") + } +} diff --git a/plugins/stronghold/src/lib.rs b/plugins/stronghold/src/lib.rs new file mode 100644 index 0000000..95ea715 --- /dev/null +++ b/plugins/stronghold/src/lib.rs @@ -0,0 +1,533 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +//! Store secrets and keys using the [IOTA Stronghold](https://github.com/iotaledger/stronghold.rs) encrypted database and secure runtime. +//! +//! ## Cargo features +//! +//! - **kdf** *(enabled by default)*: Exposes the key-derivation module and provides the default Argon2-based password-to-key derivation. + +#![doc( + html_logo_url = "https://github.com/tauri-apps/tauri/raw/dev/app-icon.png", + html_favicon_url = "https://github.com/tauri-apps/tauri/raw/dev/app-icon.png" +)] + +use std::{ + collections::HashMap, + fmt, + path::PathBuf, + sync::{Arc, Mutex}, + time::Duration, +}; + +use crypto::keys::bip39; +use iota_stronghold::{ + procedures::{ + BIP39Generate, BIP39Recover, Curve, Ed25519Sign, KeyType as StrongholdKeyType, + MnemonicLanguage, PublicKey, Slip10Derive, Slip10DeriveInput, Slip10Generate, + StrongholdProcedure, + }, + Client, Location, +}; +use serde::{de::Visitor, Deserialize, Deserializer}; +use stronghold::{Error, Result, Stronghold}; +use tauri::{ + plugin::{Builder as PluginBuilder, TauriPlugin}, + Manager, Runtime, State, +}; +use zeroize::{Zeroize, Zeroizing}; + +#[cfg(feature = "kdf")] +pub mod kdf; + +pub mod stronghold; + +type PasswordHashFn = dyn Fn(&str) -> Vec + Send + Sync; + +#[derive(Default)] +struct StrongholdCollection(Arc>>); + +struct PasswordHashFunction(Box); + +#[derive(Deserialize, Hash, Eq, PartialEq, Ord, PartialOrd)] +#[serde(untagged)] +enum BytesDto { + Text(String), + Raw(Vec), +} + +impl AsRef<[u8]> for BytesDto { + fn as_ref(&self) -> &[u8] { + match self { + Self::Text(t) => t.as_ref(), + Self::Raw(b) => b.as_ref(), + } + } +} + +impl From for Vec { + fn from(v: BytesDto) -> Self { + match v { + BytesDto::Text(t) => t.as_bytes().to_vec(), + BytesDto::Raw(b) => b, + } + } +} + +#[derive(Deserialize)] +#[serde(tag = "type", content = "payload")] +enum LocationDto { + Generic { vault: BytesDto, record: BytesDto }, + Counter { vault: BytesDto, counter: usize }, +} + +impl From for Location { + fn from(dto: LocationDto) -> Location { + match dto { + LocationDto::Generic { vault, record } => Location::generic(vault, record), + LocationDto::Counter { vault, counter } => Location::counter(vault, counter), + } + } +} + +#[derive(Deserialize)] +#[serde(tag = "type", content = "payload")] +#[allow(clippy::upper_case_acronyms)] +enum Slip10DeriveInputDto { + Seed(LocationDto), + Key(LocationDto), +} + +impl From for Slip10DeriveInput { + fn from(dto: Slip10DeriveInputDto) -> Slip10DeriveInput { + match dto { + Slip10DeriveInputDto::Seed(location) => Slip10DeriveInput::Seed(location.into()), + Slip10DeriveInputDto::Key(location) => Slip10DeriveInput::Key(location.into()), + } + } +} + +/// The type of a key pair handled by the plugin procedures. +/// +/// Deserialized from the strings `ed25519` and `x25519`, ignoring case. +pub enum KeyType { + /// The Ed25519 signature scheme. + Ed25519, + /// The X25519 key exchange scheme. + X25519, +} + +impl From for StrongholdKeyType { + fn from(ty: KeyType) -> StrongholdKeyType { + match ty { + KeyType::Ed25519 => StrongholdKeyType::Ed25519, + KeyType::X25519 => StrongholdKeyType::X25519, + } + } +} + +impl<'de> Deserialize<'de> for KeyType { + fn deserialize(deserializer: D) -> std::result::Result + where + D: Deserializer<'de>, + { + struct KeyTypeVisitor; + + impl Visitor<'_> for KeyTypeVisitor { + type Value = KeyType; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("ed25519 or x25519") + } + + fn visit_str(self, value: &str) -> std::result::Result + where + E: serde::de::Error, + { + match value.to_lowercase().as_str() { + "ed25519" => Ok(KeyType::Ed25519), + "x25519" => Ok(KeyType::X25519), + _ => Err(serde::de::Error::custom("unknown key type")), + } + } + } + + deserializer.deserialize_str(KeyTypeVisitor) + } +} + +#[derive(Deserialize)] +#[serde(tag = "type", content = "payload")] +#[allow(clippy::upper_case_acronyms)] +enum ProcedureDto { + SLIP10Generate { + output: LocationDto, + #[serde(rename = "sizeBytes")] + size_bytes: Option, + }, + SLIP10Derive { + chain: Vec, + input: Slip10DeriveInputDto, + output: LocationDto, + }, + BIP39Recover { + mnemonic: String, + passphrase: Option, + output: LocationDto, + }, + BIP39Generate { + passphrase: Option, + output: LocationDto, + }, + PublicKey { + #[serde(rename = "type")] + ty: KeyType, + #[serde(rename = "privateKey")] + private_key: LocationDto, + }, + Ed25519Sign { + #[serde(rename = "privateKey")] + private_key: LocationDto, + msg: String, + }, +} + +impl From for StrongholdProcedure { + fn from(dto: ProcedureDto) -> StrongholdProcedure { + match dto { + ProcedureDto::SLIP10Generate { output, size_bytes } => { + StrongholdProcedure::Slip10Generate(Slip10Generate { + output: output.into(), + size_bytes, + }) + } + ProcedureDto::SLIP10Derive { + chain, + input, + output, + } => StrongholdProcedure::Slip10Derive(Slip10Derive { + curve: Curve::Ed25519, + chain, + input: input.into(), + output: output.into(), + }), + ProcedureDto::BIP39Recover { + mnemonic, + passphrase, + output, + } => StrongholdProcedure::BIP39Recover(BIP39Recover { + mnemonic: bip39::Mnemonic::from(mnemonic), + passphrase: bip39::Passphrase::from(passphrase.unwrap_or_default()), + output: output.into(), + }), + ProcedureDto::BIP39Generate { passphrase, output } => { + StrongholdProcedure::BIP39Generate(BIP39Generate { + passphrase: bip39::Passphrase::from(passphrase.unwrap_or_default()), + output: output.into(), + language: MnemonicLanguage::English, + }) + } + ProcedureDto::PublicKey { ty, private_key } => { + StrongholdProcedure::PublicKey(PublicKey { + ty: ty.into(), + private_key: private_key.into(), + }) + } + ProcedureDto::Ed25519Sign { private_key, msg } => { + StrongholdProcedure::Ed25519Sign(Ed25519Sign { + private_key: private_key.into(), + msg: msg.as_bytes().to_vec(), + }) + } + } + } +} + +#[tauri::command] +async fn initialize( + collection: State<'_, StrongholdCollection>, + hash_function: State<'_, PasswordHashFunction>, + snapshot_path: PathBuf, + mut password: String, +) -> Result<()> { + let hash = (hash_function.0)(&password); + password.zeroize(); + let stronghold = Stronghold::new(snapshot_path.clone(), hash)?; + + collection + .0 + .lock() + .unwrap() + .insert(snapshot_path, stronghold); + + Ok(()) +} + +#[tauri::command] +async fn destroy( + collection: State<'_, StrongholdCollection>, + snapshot_path: PathBuf, +) -> Result<()> { + let mut collection = collection.0.lock().unwrap(); + if let Some(stronghold) = collection.remove(&snapshot_path) { + if let Err(e) = stronghold.save() { + collection.insert(snapshot_path, stronghold); + return Err(e); + } + } + Ok(()) +} + +#[tauri::command] +async fn save(collection: State<'_, StrongholdCollection>, snapshot_path: PathBuf) -> Result<()> { + let collection = collection.0.lock().unwrap(); + if let Some(stronghold) = collection.get(&snapshot_path) { + stronghold.save()?; + } + Ok(()) +} + +#[tauri::command] +async fn create_client( + collection: State<'_, StrongholdCollection>, + snapshot_path: PathBuf, + client: BytesDto, +) -> Result<()> { + let stronghold = get_stronghold(collection, snapshot_path)?; + stronghold.create_client(client)?; + Ok(()) +} + +#[tauri::command] +async fn load_client( + collection: State<'_, StrongholdCollection>, + snapshot_path: PathBuf, + client: BytesDto, +) -> Result<()> { + let stronghold = get_stronghold(collection, snapshot_path)?; + stronghold.load_client(client)?; + Ok(()) +} + +#[tauri::command] +async fn get_store_record( + collection: State<'_, StrongholdCollection>, + snapshot_path: PathBuf, + client: BytesDto, + key: String, +) -> Result>> { + let client = get_client(collection, snapshot_path, client)?; + client.store().get(key.as_ref()).map_err(Into::into) +} + +#[tauri::command] +async fn save_store_record( + collection: State<'_, StrongholdCollection>, + snapshot_path: PathBuf, + client: BytesDto, + key: String, + value: Vec, + lifetime: Option, +) -> Result>> { + let client = get_client(collection, snapshot_path, client)?; + client + .store() + .insert(key.as_bytes().to_vec(), value, lifetime) + .map_err(Into::into) +} + +#[tauri::command] +async fn remove_store_record( + collection: State<'_, StrongholdCollection>, + snapshot_path: PathBuf, + client: BytesDto, + key: String, +) -> Result>> { + let client = get_client(collection, snapshot_path, client)?; + client.store().delete(key.as_ref()).map_err(Into::into) +} + +#[tauri::command] +async fn save_secret( + collection: State<'_, StrongholdCollection>, + snapshot_path: PathBuf, + client: BytesDto, + vault: BytesDto, + record_path: BytesDto, + secret: Vec, +) -> Result<()> { + let client = get_client(collection, snapshot_path, client)?; + client + .vault(&vault) + .write_secret( + Location::generic(vault, record_path), + Zeroizing::new(secret), + ) + .map_err(Into::into) +} + +#[tauri::command] +async fn remove_secret( + collection: State<'_, StrongholdCollection>, + snapshot_path: PathBuf, + client: BytesDto, + vault: BytesDto, + record_path: BytesDto, +) -> Result<()> { + let client = get_client(collection, snapshot_path, client)?; + client + .vault(vault) + .delete_secret(record_path) + .map(|_| ()) + .map_err(Into::into) +} + +#[tauri::command] +async fn execute_procedure( + collection: State<'_, StrongholdCollection>, + snapshot_path: PathBuf, + client: BytesDto, + procedure: ProcedureDto, +) -> Result> { + let client = get_client(collection, snapshot_path, client)?; + client + .execute_procedure(StrongholdProcedure::from(procedure)) + .map(Into::into) + .map_err(Into::into) +} + +fn get_stronghold( + collection: State<'_, StrongholdCollection>, + snapshot_path: PathBuf, +) -> Result { + let collection = collection.0.lock().unwrap(); + if let Some(stronghold) = collection.get(&snapshot_path) { + Ok(stronghold.inner().clone()) + } else { + Err(Error::StrongholdNotInitialized) + } +} + +fn get_client( + collection: State<'_, StrongholdCollection>, + snapshot_path: PathBuf, + client: BytesDto, +) -> Result { + let collection = collection.0.lock().unwrap(); + if let Some(stronghold) = collection.get(&snapshot_path) { + stronghold.get_client(client).map_err(Into::into) + } else { + Err(Error::StrongholdNotInitialized) + } +} + +enum PasswordHashFunctionKind { + #[cfg(feature = "kdf")] + Argon2(PathBuf), + Custom(Box), +} + +/// Builder for the stronghold plugin. +/// +/// It defines how the password sent by the frontend is hashed into the key that +/// encrypts the snapshot file. +pub struct Builder { + password_hash_function: PasswordHashFunctionKind, +} + +impl Builder { + /// Initializes [`Self`] with a custom password hash function. + /// + /// The function is called with the password sent by the frontend and must return the + /// key used to encrypt the snapshot, which must be 32 bytes long. + /// + /// # Examples + /// + /// ```rust + /// fn init(builder: tauri::Builder) -> tauri::Builder { + /// builder.plugin( + /// tauri_plugin_stronghold::Builder::new(|_password| { + /// // hash the password with a secure algorithm such as argon2 or blake2b + /// // and return the resulting 32 bytes hash + /// unimplemented!() + /// }) + /// .build(), + /// ) + /// } + /// ``` + pub fn new Vec + Send + Sync + 'static>(password_hash_function: F) -> Self { + Self { + password_hash_function: PasswordHashFunctionKind::Custom(Box::new( + password_hash_function, + )), + } + } + + /// Initializes [`Self`] with argon2 as password hash function. + /// + /// # Examples + /// + /// ```rust + /// use tauri::Manager; + /// + /// fn init(builder: tauri::Builder) -> tauri::Builder { + /// builder.setup(|app| { + /// let salt_path = app + /// .path() + /// .app_local_data_dir() + /// .expect("could not resolve app local data path") + /// .join("salt.txt"); + /// app.handle() + /// .plugin(tauri_plugin_stronghold::Builder::with_argon2(&salt_path).build())?; + /// Ok(()) + /// }) + /// } + /// ``` + #[cfg(feature = "kdf")] + pub fn with_argon2(salt_path: &std::path::Path) -> Self { + Self { + password_hash_function: PasswordHashFunctionKind::Argon2(salt_path.to_owned()), + } + } + + /// Builds the plugin, registering the password hash function and the commands used + /// by the JavaScript guest bindings. + pub fn build(self) -> TauriPlugin { + let password_hash_function = self.password_hash_function; + + let plugin_builder = PluginBuilder::new("stronghold").setup(move |app, _api| { + app.manage(StrongholdCollection::default()); + app.manage(PasswordHashFunction(match password_hash_function { + #[cfg(feature = "kdf")] + PasswordHashFunctionKind::Argon2(path) => { + Box::new(move |p| kdf::KeyDerivation::argon2(p, &path)) + } + PasswordHashFunctionKind::Custom(f) => f, + })); + Ok(()) + }); + + Builder::invoke_stronghold_handlers_and_build(plugin_builder) + } + + fn invoke_stronghold_handlers_and_build( + builder: PluginBuilder, + ) -> TauriPlugin { + builder + .invoke_handler(tauri::generate_handler![ + initialize, + destroy, + save, + create_client, + load_client, + get_store_record, + save_store_record, + remove_store_record, + save_secret, + remove_secret, + execute_procedure, + ]) + .build() + } +} diff --git a/plugins/stronghold/src/stronghold.rs b/plugins/stronghold/src/stronghold.rs new file mode 100644 index 0000000..3e85684 --- /dev/null +++ b/plugins/stronghold/src/stronghold.rs @@ -0,0 +1,103 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +//! Types to load, modify and persist an [IOTA Stronghold](https://github.com/iotaledger/stronghold.rs) +//! snapshot file. + +use std::{convert::TryFrom, ops::Deref, path::Path}; + +use iota_stronghold::{KeyProvider, SnapshotPath}; +use serde::{Serialize, Serializer}; +use zeroize::Zeroizing; + +/// Alias for a [`std::result::Result`] with the error type [`Error`]. +pub type Result = std::result::Result; + +/// Errors returned by the stronghold plugin. +/// +/// Serialized as the error message string when returned to the frontend. +#[derive(Debug, thiserror::Error)] +pub enum Error { + /// No stronghold was initialized for the given snapshot path. + #[error("stronghold not initialized")] + StrongholdNotInitialized, + /// An error from the underlying Stronghold client, e.g. when loading a snapshot + /// with the wrong password or when addressing a client that does not exist. + #[error(transparent)] + Stronghold(#[from] iota_stronghold::ClientError), + /// An error from the Stronghold secure memory implementation, e.g. when the + /// password hash is not a key size Stronghold accepts. + #[error(transparent)] + Memory(#[from] iota_stronghold::MemoryError), + /// A Stronghold procedure (key generation, key derivation, signing, ...) failed. + #[error(transparent)] + Procedure(#[from] iota_stronghold::procedures::ProcedureError), +} + +impl Serialize for Error { + fn serialize(&self, serializer: S) -> std::result::Result + where + S: Serializer, + { + serializer.serialize_str(self.to_string().as_str()) + } +} + +/// A Stronghold instance bound to a snapshot file and to the key it is encrypted with. +/// +/// Dereferences to the underlying [`iota_stronghold::Stronghold`], so all of its client +/// and vault operations are available on this type. +pub struct Stronghold { + inner: iota_stronghold::Stronghold, + path: SnapshotPath, + keyprovider: KeyProvider, +} + +impl Stronghold { + /// Creates a Stronghold instance for the snapshot file at `path`, encrypted with + /// `password` as the key. + /// + /// When the file already exists its snapshot is loaded, which requires `password` to + /// be the key it was encrypted with. Otherwise an empty instance is created and + /// nothing is written to disk until [`Self::save`] is called. + /// + /// # Errors + /// + /// Returns [`Error::Memory`] when `password` is not a key size Stronghold accepts + /// (it must be 32 bytes long) and [`Error::Stronghold`] when an existing snapshot + /// cannot be loaded with it. + pub fn new>(path: P, password: Vec) -> Result { + let path = SnapshotPath::from_path(path); + let stronghold = iota_stronghold::Stronghold::default(); + let keyprovider = KeyProvider::try_from(Zeroizing::new(password))?; + if path.exists() { + stronghold.load_snapshot(&keyprovider, &path)?; + } + Ok(Self { + inner: stronghold, + path, + keyprovider, + }) + } + + /// Writes the state of all clients to the snapshot file, encrypted with the key + /// this instance was created with. + pub fn save(&self) -> Result<()> { + self.inner + .commit_with_keyprovider(&self.path, &self.keyprovider)?; + Ok(()) + } + + /// Returns a reference to the underlying [`iota_stronghold::Stronghold`] instance. + pub fn inner(&self) -> &iota_stronghold::Stronghold { + &self.inner + } +} + +impl Deref for Stronghold { + type Target = iota_stronghold::Stronghold; + fn deref(&self) -> &Self::Target { + &self.inner + } +} diff --git a/plugins/stronghold/tsconfig.json b/plugins/stronghold/tsconfig.json new file mode 100644 index 0000000..4374468 --- /dev/null +++ b/plugins/stronghold/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "rootDir": "guest-js" + }, + "include": ["guest-js/*.ts"] +} diff --git a/plugins/updater/CHANGELOG.md b/plugins/updater/CHANGELOG.md new file mode 100644 index 0000000..6a5484a --- /dev/null +++ b/plugins/updater/CHANGELOG.md @@ -0,0 +1,257 @@ +# Changelog + +## [2.12.0] + +- [`1308bfa3`](https://github.com/tauri-apps/plugins-workspace/commit/1308bfa399b962b3977c767100a6339d1cbfdd20) **Breaking change:** the `allowDowngrades` option was removed from the `check` command and is now read from the plugin configuration instead. + + Previously any code running in the webview could pass `allowDowngrades: true` to `plugin:updater|check` and relax the version check from "the update must be newer" to "the update must be different", overriding the comparator the application had configured on the Rust side. The flag is now an application-level setting: + + ```json + { + "plugins": { + "updater": { + "allowDowngrades": true + } + } + } + ``` + + It defaults to `false`, and is ignored when the application provides its own `Builder::default_version_comparator`, which continues to take precedence. + +## [2.11.0] + +- [`f8053e65`](https://github.com/tauri-apps/plugins-workspace/commit/f8053e659e4ccd85c1f52833411ff8417cbc5e69) ([#3527](https://github.com/tauri-apps/plugins-workspace/pull/3527) by [@Legend-Master](https://github.com/tauri-apps/plugins-workspace/../../Legend-Master)) Documented Cargo feature flags in each plugin's crate-level documentation. +- [`622f02bf`](https://github.com/tauri-apps/plugins-workspace/commit/622f02bf21858f0cff95419fc042ce02b8c6b18b) ([#3516](https://github.com/tauri-apps/plugins-workspace/pull/3516) by [@Legend-Master](https://github.com/tauri-apps/plugins-workspace/../../Legend-Master)) On Windows, returns an error if failed to spawn the installer in `install` through `ShellExecuteW` +- [`ab7489c9`](https://github.com/tauri-apps/plugins-workspace/commit/ab7489c964977c2cc19cdc303bd8f7142d5f5a2c) ([#3299](https://github.com/tauri-apps/plugins-workspace/pull/3299) by [@Legend-Master](https://github.com/tauri-apps/plugins-workspace/../../Legend-Master)) On Windows, add a new option `restartAfterInstall`/`restart_after_install` to install an update without the installer re-launching the app +- [`2371be83`](https://github.com/tauri-apps/plugins-workspace/commit/2371be839f9ae46dd2b0529a2d1598f2b6270ab1) ([#3526](https://github.com/tauri-apps/plugins-workspace/pull/3526) by [@Legend-Master](https://github.com/tauri-apps/plugins-workspace/../../Legend-Master)) Added `system-proxy` (enabled by default) feature flag to control `reqwest/system-proxy` feature (Use Windows and macOS system proxy settings automatically) + +## \[2.10.1] + +- [`31ab6f8d`](https://github.com/tauri-apps/plugins-workspace/commit/31ab6f8d2466d86c80b1d70510c0400ce2cdcb0a) ([#3285](https://github.com/tauri-apps/plugins-workspace/pull/3285) by [@hrzlgnm](https://github.com/tauri-apps/plugins-workspace/../../hrzlgnm)) fix: preserve file extension of updater package, otherwise users may get confused when presented with a sudo dialog suggesting to install a file with the extension `.rpm` using `dpkg -i` + +## \[2.10.0] + +- [`4375c98b`](https://github.com/tauri-apps/plugins-workspace/commit/4375c98bed1769a1fc1be26eded4db9f7c095d95) ([#3073](https://github.com/tauri-apps/plugins-workspace/pull/3073)) Add no_proxy config to disable system proxy for updater plugin. +- [`f122ee98`](https://github.com/tauri-apps/plugins-workspace/commit/f122ee98c6954075891ac3cd530f5e29da117b39) Allow configuring the updater client to accept invalid TLS certificates and hostnames for internal/self-signed update servers. These options are available via the plugin config (`dangerousAcceptInvalidCerts`, `dangerousAcceptInvalidHostnames`) and via the `UpdaterBuilder` (`dangerous_accept_invalid_certs`, `dangerous_accept_invalid_hostnames`). +- [`f122ee98`](https://github.com/tauri-apps/plugins-workspace/commit/f122ee98c6954075891ac3cd530f5e29da117b39) Updater plugin now supports all bundle types: Deb, Rpm and AppImage for Linux; NSiS, MSI for Windows. This was added in https://github.com/tauri-apps/plugins-workspace/pull/2624 +- [`05c5da07`](https://github.com/tauri-apps/plugins-workspace/commit/05c5da072b6e3254c19ee69024f80f4dfe1b779b) ([#3213](https://github.com/tauri-apps/plugins-workspace/pull/3213)) Updated `reqwest` to 0.13, make sure to update your `reqwest` dependency if you're using `UpdaterBuilder::configure_client` + +## \[2.9.0] + +- [`f209b2f2`](https://github.com/tauri-apps/plugins-workspace/commit/f209b2f23cb29133c97ad5961fb46ef794dbe063) ([#2804](https://github.com/tauri-apps/plugins-workspace/pull/2804) by [@renovate](https://github.com/tauri-apps/plugins-workspace/../../renovate)) Updated tauri to 2.6 + +## \[2.8.1] + +- [`735d209d`](https://github.com/tauri-apps/plugins-workspace/commit/735d209d5d1d92dcac70c6083bcfcd34ec7d84be) ([#2761](https://github.com/tauri-apps/plugins-workspace/pull/2761) by [@FabianLars](https://github.com/tauri-apps/plugins-workspace/../../FabianLars)) Fixed an issue preventing updates via the NSIS installer from succeeding when the app was launched with command line arguments containing spaces. + +## \[2.8.0] + +- [`87afa23c`](https://github.com/tauri-apps/plugins-workspace/commit/87afa23cad077c09bc1eb743800ae3396b531146) ([#2726](https://github.com/tauri-apps/plugins-workspace/pull/2726)) Add allowDowngrades parameter to check command + + Added a new optional `allowDowngrades` parameter to the JavaScript check command that allows the updater to consider versions that are lower than the current version as valid updates. When enabled, the version comparator will accept any version that is different from the current version, effectively allowing downgrades. +- [`73ff15de`](https://github.com/tauri-apps/plugins-workspace/commit/73ff15de5d07d476693e40e8e5d138c16da5211e) ([#2757](https://github.com/tauri-apps/plugins-workspace/pull/2757)) Fix headers option in `Update.download` and `Update.downloadAndInstall` doesn't work with `Record | Headers` types + +## \[2.7.1] + +- [`c5b0f51c`](https://github.com/tauri-apps/plugins-workspace/commit/c5b0f51cfd911cca9317b59efc718b570980129b) ([#2621](https://github.com/tauri-apps/plugins-workspace/pull/2621) by [@Legend-Master](https://github.com/tauri-apps/plugins-workspace/../../Legend-Master)) Fix `check` and `download` overrides the `accept` header + +## \[2.7.0] + +### bug + +- [`2d731f80`](https://github.com/tauri-apps/plugins-workspace/commit/2d731f80224f74faf1b7170b25e04f5da1da49c8) ([#2573](https://github.com/tauri-apps/plugins-workspace/pull/2573)) Fix JS API `Update.date` not formatted to RFC 3339 +- [`0bc5d588`](https://github.com/tauri-apps/plugins-workspace/commit/0bc5d5887420ba1eb718254490b7995c771c0447) ([#2572](https://github.com/tauri-apps/plugins-workspace/pull/2572)) Fix `timeout` passed to `check` gets re-used by `download` and `downloadAndinstall` + +## \[2.6.1] + +- [`12c4537b`](https://github.com/tauri-apps/plugins-workspace/commit/12c4537b8e4fed29b415ff817434b664c0596dac) ([#2541](https://github.com/tauri-apps/plugins-workspace/pull/2541) by [@lucasfernog](https://github.com/tauri-apps/plugins-workspace/../../lucasfernog)) Add support to the `riscv64` architecture. + +## \[2.6.0] + +- [`faefcc9f`](https://github.com/tauri-apps/plugins-workspace/commit/faefcc9fd8c61f709d491649e255a7fcac82c09a) ([#2430](https://github.com/tauri-apps/plugins-workspace/pull/2430) by [@goenning](https://github.com/tauri-apps/plugins-workspace/../../goenning)) Add `UpdaterBuilder::configure_client` method on Rust side, to configure the `reqwest` client used to check and download the update. +- [`ac60d589`](https://github.com/tauri-apps/plugins-workspace/commit/ac60d589eca2bbc4aed040feb18da148e66ec171) ([#2513](https://github.com/tauri-apps/plugins-workspace/pull/2513) by [@lucasfernog](https://github.com/tauri-apps/plugins-workspace/../../lucasfernog)) Enhance error logging. + +## \[2.5.1] + +- [`6f881293`](https://github.com/tauri-apps/plugins-workspace/commit/6f881293fcd67838f6f3f8063f536292431dd1f7) ([#2439](https://github.com/tauri-apps/plugins-workspace/pull/2439) by [@FabianLars](https://github.com/tauri-apps/plugins-workspace/../../FabianLars)) Fixed an issue that caused the plugin to emit a `ReleaseNotFound` error instead of a `Reqwest` error when the http request in `check()` failed. + +## \[2.5.0] + +- [`5369898d`](https://github.com/tauri-apps/plugins-workspace/commit/5369898db7a6098e3e2f43436100ea556d405628) ([#2067](https://github.com/tauri-apps/plugins-workspace/pull/2067) by [@jLynx](https://github.com/tauri-apps/plugins-workspace/../../jLynx)) Fix update installation on macOS when using an user without admin privileges. +- [`5369898d`](https://github.com/tauri-apps/plugins-workspace/commit/5369898db7a6098e3e2f43436100ea556d405628) ([#2067](https://github.com/tauri-apps/plugins-workspace/pull/2067) by [@jLynx](https://github.com/tauri-apps/plugins-workspace/../../jLynx)) Remove the `UpdaterBuilder::new` function, use `UpdaterExt::updater_builder` instead. + +## \[2.4.0] + +- [`0afc9b6b`](https://github.com/tauri-apps/plugins-workspace/commit/0afc9b6be07bee1077f05a86285d977e57810ed9) ([#2325](https://github.com/tauri-apps/plugins-workspace/pull/2325) by [@FabianLars](https://github.com/tauri-apps/plugins-workspace/../../FabianLars)) The `Update` struct/object will now contain a `raw_json`/`rawJson` property to be able to read parts of server's json response that are not handled by the plugin. + +## \[2.3.1] + +- [`57efb47c`](https://github.com/tauri-apps/plugins-workspace/commit/57efb47c116f880477f72f02a8e4239e88007d44) ([#2235](https://github.com/tauri-apps/plugins-workspace/pull/2235) by [@amrbashir](https://github.com/tauri-apps/plugins-workspace/../../amrbashir)) Add `Builder::header` and `Builder::headers` method to configure default headers for updater. + +## \[2.3.0] + +- [`829b6326`](https://github.com/tauri-apps/plugins-workspace/commit/829b63265030bc9c61d1738c4eaca0ffb3178677) ([#1919](https://github.com/tauri-apps/plugins-workspace/pull/1919) by [@n1ght-hunter](https://github.com/tauri-apps/plugins-workspace/../../n1ght-hunter)) Add `tauri_plugin_updater::Builder::default_version_comparator` method to set the default version comparator for the updater. + +## \[2.2.0] + +- [`3a79266b`](https://github.com/tauri-apps/plugins-workspace/commit/3a79266b8cf96a55b1ae6339d725567d45a44b1d) ([#2173](https://github.com/tauri-apps/plugins-workspace/pull/2173) by [@FabianLars](https://github.com/tauri-apps/plugins-workspace/../../FabianLars)) Bumped all plugins to `v2.2.0`. From now, the versions for the Rust and JavaScript packages of each plugin will be in sync with each other. + +## \[2.1.0] + +- [`f8f2eefe`](https://github.com/tauri-apps/plugins-workspace/commit/f8f2eefe03ab231beafbd6a88d61b53d77f0400d) ([#1991](https://github.com/tauri-apps/plugins-workspace/pull/1991) by [@jLynx](https://github.com/tauri-apps/plugins-workspace/../../jLynx)) Added support for `.deb` package updates on Linux systems. + +## \[2.0.2] + +- [`a1a82208`](https://github.com/tauri-apps/plugins-workspace/commit/a1a82208ed4ab87f83310be0dc95428aec9ab241) ([#1873](https://github.com/tauri-apps/plugins-workspace/pull/1873) by [@lucasfernog](https://github.com/tauri-apps/plugins-workspace/../../lucasfernog)) Downgrade MSRV to 1.77.2 to support Windows 7. + +## \[2.0.1] + +- [`9501cfa5`](https://github.com/tauri-apps/plugins-workspace/commit/9501cfa5f5385b2d7eb43a8378b322ee97cba06f) ([#1868](https://github.com/tauri-apps/plugins-workspace/pull/1868) by [@lucasfernog](https://github.com/tauri-apps/plugins-workspace/../../lucasfernog)) Fix configuration parser incorrectly warning about the endpoint scheme. + +## \[2.0.0] + +- [`e2c4dfb6`](https://github.com/tauri-apps/plugins-workspace/commit/e2c4dfb6af43e5dd8d9ceba232c315f5febd55c1) Update to tauri v2 stable release. + +## \[2.0.0-rc.4] + +- [`221f50f5`](https://github.com/tauri-apps/plugins-workspace/commit/221f50f53bd7a87dbd404e4cb1aaf502a5047785) ([#1816](https://github.com/tauri-apps/plugins-workspace/pull/1816) by [@amrbashir](https://github.com/tauri-apps/plugins-workspace/../../amrbashir)) Encode `+` when making updater requests which can be cause incorrectly interpolating the endpoint when using `{{current_version}}` in the endpoint where the current version contains a build number, for example `1.8.0+1`. +- [`04a0aea0`](https://github.com/tauri-apps/plugins-workspace/commit/04a0aea0ab9f8750200bc2fe5aff99c1c488082d) ([#1814](https://github.com/tauri-apps/plugins-workspace/pull/1814) by [@amrbashir](https://github.com/tauri-apps/plugins-workspace/../../amrbashir)) **Breaking change**, Changed `UpdaterBuilder::endpoints` method to return a `Result`. +- [`04a0aea0`](https://github.com/tauri-apps/plugins-workspace/commit/04a0aea0ab9f8750200bc2fe5aff99c1c488082d) ([#1814](https://github.com/tauri-apps/plugins-workspace/pull/1814) by [@amrbashir](https://github.com/tauri-apps/plugins-workspace/../../amrbashir)) Add `dangerousInsecureTransportProtocol` config option to allow using insecure transport protocols, like `http` + +## \[2.0.0-rc.3] + +- [`d00519e3`](https://github.com/tauri-apps/plugins-workspace/commit/d00519e3e3a3234f9eb6c2ba82c92d4199f03e53) ([#1735](https://github.com/tauri-apps/plugins-workspace/pull/1735) by [@FabianLars](https://github.com/tauri-apps/plugins-workspace/../../FabianLars)) This releases the changes from 2.0.0-rc.2 to crates.io. Please see the links below for the actual changes. + +## \[2.0.0-rc.2] + +- [`f8255e1d`](https://github.com/tauri-apps/plugins-workspace/commit/f8255e1db5df6cf562b9334fbefe5e62f4a28e0a) ([#1661](https://github.com/tauri-apps/plugins-workspace/pull/1661) by [@amrbashir](https://github.com/tauri-apps/plugins-workspace/../../amrbashir)) Add a second argument in `Update.download` and `Update.donloadAndInstall` JS APIs to modify headers and timeout when downloading the update. + +## \[2.0.0-rc.1] + +- [`e2e97db5`](https://github.com/tauri-apps/plugins-workspace/commit/e2e97db51983267f5be84d4f6f0278d58834d1f5) ([#1701](https://github.com/tauri-apps/plugins-workspace/pull/1701) by [@lucasfernog](https://github.com/tauri-apps/plugins-workspace/../../lucasfernog)) Update to tauri 2.0.0-rc.8 + +## \[2.0.0-rc.1] + +- [`77013925`](https://github.com/tauri-apps/plugins-workspace/commit/7701392500f375340045880fce5fb8f867bfe670) ([#1636](https://github.com/tauri-apps/plugins-workspace/pull/1636) by [@lucasfernog](https://github.com/tauri-apps/plugins-workspace/../../lucasfernog)) Fixes the updater not preserving AppImage file permissions. +- [`5d170a54`](https://github.com/tauri-apps/plugins-workspace/commit/5d170a5444982dcc14135f6f1fc3e5da359f0eb0) ([#1671](https://github.com/tauri-apps/plugins-workspace/pull/1671) by [@lucasfernog](https://github.com/tauri-apps/plugins-workspace/../../lucasfernog)) Update to tauri 2.0.0-rc.3. + +## \[2.0.0-rc.0] + +- [`9887d1`](https://github.com/tauri-apps/plugins-workspace/commit/9887d14bd0e971c4c0f5c1188fc4005d3fc2e29e) Update to tauri RC. + +## \[2.0.0-beta.8] + +- [`99d6ac0f`](https://github.com/tauri-apps/plugins-workspace/commit/99d6ac0f9506a6a4a1aa59c728157190a7441af6) ([#1606](https://github.com/tauri-apps/plugins-workspace/pull/1606) by [@FabianLars](https://github.com/tauri-apps/plugins-workspace/../../FabianLars)) The JS packages now specify the *minimum* `@tauri-apps/api` version instead of a single exact version. +- [`6de87966`](https://github.com/tauri-apps/plugins-workspace/commit/6de87966ecc00ad9d91c25be452f1f46bd2b7e1f) ([#1597](https://github.com/tauri-apps/plugins-workspace/pull/1597) by [@Legend-Master](https://github.com/tauri-apps/plugins-workspace/../../Legend-Master)) Update to tauri beta.25. + +## \[2.0.0-beta.11] + +- [`f83b9e98`](https://github.com/tauri-apps/plugins-workspace/commit/f83b9e9813843df19b03b6af1018d848111b2a62) ([#1544](https://github.com/tauri-apps/plugins-workspace/pull/1544) by [@Legend-Master](https://github.com/tauri-apps/plugins-workspace/../../Legend-Master)) On Windows, use a named tempfile with `--installer.exe` (or `.msi`) for v2 updater + + **Breaking Change**: `UpdaterBuilder::new` now takes one more argument `app_name: String` + +## \[2.0.0-beta.7] + +- [`22a17980`](https://github.com/tauri-apps/plugins-workspace/commit/22a17980ff4f6f8c40adb1b8f4ffc6dae2fe7e30) ([#1537](https://github.com/tauri-apps/plugins-workspace/pull/1537) by [@lucasfernog](https://github.com/tauri-apps/plugins-workspace/../../lucasfernog)) Update to tauri beta.24. + +## \[2.0.0-beta.6] + +- [`76daee7a`](https://github.com/tauri-apps/plugins-workspace/commit/76daee7aafece34de3092c86e531cf9eb1138989) ([#1512](https://github.com/tauri-apps/plugins-workspace/pull/1512) by [@renovate](https://github.com/tauri-apps/plugins-workspace/../../renovate)) Update to tauri beta.23. + +## \[2.0.0-beta.8] + +- [`bf29a72b`](https://github.com/tauri-apps/plugins-workspace/commit/bf29a72baaff15214a21989df23081eee84e3b8b) ([#1454](https://github.com/tauri-apps/plugins-workspace/pull/1454) by [@amrbashir](https://github.com/tauri-apps/plugins-workspace/../../amrbashir)) Fix regression in updater plugin failing to update using `.msi` installer. + +## \[2.0.0-beta.5] + +- [`9013854f`](https://github.com/tauri-apps/plugins-workspace/commit/9013854f42a49a230b9dbb9d02774765528a923f)([#1382](https://github.com/tauri-apps/plugins-workspace/pull/1382)) Update to tauri beta.22. + +## \[2.0.0-beta.4] + +- [`430bd6f4`](https://github.com/tauri-apps/plugins-workspace/commit/430bd6f4f379bee5d232ae6b098ae131db7f178a)([#1363](https://github.com/tauri-apps/plugins-workspace/pull/1363)) Update to tauri beta.20. +- [`43224c5d`](https://github.com/tauri-apps/plugins-workspace/commit/43224c5d5cfe2dd676e79ebafe424027c62c51c3)([#1330](https://github.com/tauri-apps/plugins-workspace/pull/1330)) Add `Update.download` and `Update.install` functions to the JavaScript API + +## \[2.0.0-beta.3] + +- [`bd1ed590`](https://github.com/tauri-apps/plugins-workspace/commit/bd1ed5903ffcce5500310dac1e59e8c67674ef1e)([#1237](https://github.com/tauri-apps/plugins-workspace/pull/1237)) Update to tauri beta.17. + +## \[2.0.0-beta.4] + +- [`293f363`](https://github.com/tauri-apps/plugins-workspace/commit/293f363c0dccc43e8403729fdc8cc2b4311c2d5b)([#1175](https://github.com/tauri-apps/plugins-workspace/pull/1175)) Add a `on_before_exit` hook for cleanup before spawning the updater on Windows, defaults to `app.cleanup_before_exit` when used through `UpdaterExt` +- [`293f363`](https://github.com/tauri-apps/plugins-workspace/commit/293f363c0dccc43e8403729fdc8cc2b4311c2d5b)([#1175](https://github.com/tauri-apps/plugins-workspace/pull/1175)) **Breaking change:** The `rustls-tls` feature flag is now enabled by default. +- [`e3d41f4`](https://github.com/tauri-apps/plugins-workspace/commit/e3d41f4011bd3ea3ce281bb38bbe31d3709f8e0f)([#1191](https://github.com/tauri-apps/plugins-workspace/pull/1191)) Internally use the webview scoped resources table instead of the app one, so other webviews can't access other webviews resources. +- [`7e2fcc5`](https://github.com/tauri-apps/plugins-workspace/commit/7e2fcc5e74df7c3c718e40f75bfb0eafc7d69d8d)([#1146](https://github.com/tauri-apps/plugins-workspace/pull/1146)) Update dependencies to align with tauri 2.0.0-beta.14. +- [`e3d41f4`](https://github.com/tauri-apps/plugins-workspace/commit/e3d41f4011bd3ea3ce281bb38bbe31d3709f8e0f)([#1191](https://github.com/tauri-apps/plugins-workspace/pull/1191)) Update for tauri 2.0.0-beta.15. + +## \[2.0.0-beta.3] + +- [`4e37316`](https://github.com/tauri-apps/plugins-workspace/commit/4e37316af0d6532bf9a9bd0e712b5b14b0598285)([#1051](https://github.com/tauri-apps/plugins-workspace/pull/1051)) Fix deserialization of `windows > installerArgs` config field. +- [`4e37316`](https://github.com/tauri-apps/plugins-workspace/commit/4e37316af0d6532bf9a9bd0e712b5b14b0598285)([#1051](https://github.com/tauri-apps/plugins-workspace/pull/1051)) On Windows, fallback to `passive` install mode when not defined in config. +- [`a3b5396`](https://github.com/tauri-apps/plugins-workspace/commit/a3b5396113ca93912274f6890d9ef5b1a409587a)([#1054](https://github.com/tauri-apps/plugins-workspace/pull/1054)) Fix Windows powershell window flashing on update +- [`a04ea2f`](https://github.com/tauri-apps/plugins-workspace/commit/a04ea2f38294d5a3987578283badc8eec87a7752)([#1071](https://github.com/tauri-apps/plugins-workspace/pull/1071)) The global API script is now only added to the binary when the `withGlobalTauri` config is true. + +## \[2.0.0-beta.2] + +- [`99bea25`](https://github.com/tauri-apps/plugins-workspace/commit/99bea2559c2c0648c2519c50a18cd124dacef57b)([#1005](https://github.com/tauri-apps/plugins-workspace/pull/1005)) Update to tauri beta.8. + +## \[2.0.0-beta.1] + +- [`569defb`](https://github.com/tauri-apps/plugins-workspace/commit/569defbe9492e38938554bb7bdc1be9151456d21) Update to tauri beta.4. + +## \[2.0.0-beta.0] + +- [`d198c01`](https://github.com/tauri-apps/plugins-workspace/commit/d198c014863ee260cb0de88a14b7fc4356ef7474)([#862](https://github.com/tauri-apps/plugins-workspace/pull/862)) Update to tauri beta. +- [`0879a87`](https://github.com/tauri-apps/plugins-workspace/commit/0879a87a7ecc83c9e886e6f1412fe253082b8d34)([#899](https://github.com/tauri-apps/plugins-workspace/pull/899)) Fix `Started` event not emitted to JS when downloading update. +- [`8505a75`](https://github.com/tauri-apps/plugins-workspace/commit/8505a756b569d88757ec58e452bfe4814d8107bf)([#907](https://github.com/tauri-apps/plugins-workspace/pull/907)) Add support for specifying proxy to use for checking and downloading updates. + +## \[2.0.0-alpha.5] + +- [`387c2f9`](https://github.com/tauri-apps/plugins-workspace/commit/387c2f9e0ce4c75c07ffa3fd76391a25b58f5daf)([#802](https://github.com/tauri-apps/plugins-workspace/pull/802)) Update to @tauri-apps/api v2.0.0-alpha.13. +- [`e5f979f`](https://github.com/tauri-apps/plugins-workspace/commit/e5f979f91abbb1775fa048af3219b30ff30ed691)([#818](https://github.com/tauri-apps/plugins-workspace/pull/818)) Fix NSIS updater failing to launch when using `basicUi` mode. + +## \[2.0.0-alpha.4] + +- [`387c2f9`](https://github.com/tauri-apps/plugins-workspace/commit/387c2f9e0ce4c75c07ffa3fd76391a25b58f5daf)([#802](https://github.com/tauri-apps/plugins-workspace/pull/802)) Update to @tauri-apps/api v2.0.0-alpha.12. + +## \[2.0.0-alpha.3] + +- [`e438e0a`](https://github.com/tauri-apps/plugins-workspace/commit/e438e0a62d4b430a5159f05f13ecd397dd891a0d)([#676](https://github.com/tauri-apps/plugins-workspace/pull/676)) Update to @tauri-apps/api v2.0.0-alpha.11. + +## \[2.0.0-alpha.2] + +- [`5c13736`](https://github.com/tauri-apps/plugins-workspace/commit/5c137365c60790e8d4037d449e8237aa3fffdab0)([#673](https://github.com/tauri-apps/plugins-workspace/pull/673)) Update to @tauri-apps/api v2.0.0-alpha.9. + +## \[2.0.0-alpha.2] + +- [`4e2cef9`](https://github.com/tauri-apps/plugins-workspace/commit/4e2cef9b702bbbb9cf4ee17de50791cb21f1b2a4)([#593](https://github.com/tauri-apps/plugins-workspace/pull/593)) Update to alpha.12. + +## \[2.0.0-alpha.1] + +- [`d74fc0a`](https://github.com/tauri-apps/plugins-workspace/commit/d74fc0a097996e90a37be8f57d50b7d1f6ca616f)([#555](https://github.com/tauri-apps/plugins-workspace/pull/555)) Update to alpha.11. +- [`4ab90f0`](https://github.com/tauri-apps/plugins-workspace/commit/4ab90f048eab2918344f97dc8e04413a404e392d)([#431](https://github.com/tauri-apps/plugins-workspace/pull/431)) The updater plugin is recieving a few changes to improve consistency and ergonomics of the Rust and JS APIs + +## \[2.0.0-alpha.0] + +- [`717ae67`](https://github.com/tauri-apps/plugins-workspace/commit/717ae670978feb4492fac1f295998b93f2b9347f)([#371](https://github.com/tauri-apps/plugins-workspace/pull/371)) First v2 alpha release! +7c2f9e0ce4c75c07ffa3fd76391a25b58f5daf)([#802](https://github.com/tauri-apps/plugins-workspace/pull/802)) Update to @tauri-apps/api v2.0.0-alpha.12. + +## \[2.0.0-alpha.3] + +- [`e438e0a`](https://github.com/tauri-apps/plugins-workspace/commit/e438e0a62d4b430a5159f05f13ecd397dd891a0d)([#676](https://github.com/tauri-apps/plugins-workspace/pull/676)) Update to @tauri-apps/api v2.0.0-alpha.11. + +## \[2.0.0-alpha.2] + +- [`5c13736`](https://github.com/tauri-apps/plugins-workspace/commit/5c137365c60790e8d4037d449e8237aa3fffdab0)([#673](https://github.com/tauri-apps/plugins-workspace/pull/673)) Update to @tauri-apps/api v2.0.0-alpha.9. + +## \[2.0.0-alpha.2] + +- [`4e2cef9`](https://github.com/tauri-apps/plugins-workspace/commit/4e2cef9b702bbbb9cf4ee17de50791cb21f1b2a4)([#593](https://github.com/tauri-apps/plugins-workspace/pull/593)) Update to alpha.12. + +## \[2.0.0-alpha.1] + +- [`d74fc0a`](https://github.com/tauri-apps/plugins-workspace/commit/d74fc0a097996e90a37be8f57d50b7d1f6ca616f)([#555](https://github.com/tauri-apps/plugins-workspace/pull/555)) Update to alpha.11. +- [`4ab90f0`](https://github.com/tauri-apps/plugins-workspace/commit/4ab90f048eab2918344f97dc8e04413a404e392d)([#431](https://github.com/tauri-apps/plugins-workspace/pull/431)) The updater plugin is recieving a few changes to improve consistency and ergonomics of the Rust and JS APIs + +## \[2.0.0-alpha.0] + +- [`717ae67`](https://github.com/tauri-apps/plugins-workspace/commit/717ae670978feb4492fac1f295998b93f2b9347f)([#371](https://github.com/tauri-apps/plugins-workspace/pull/371)) First v2 alpha release! diff --git a/plugins/updater/Cargo.toml b/plugins/updater/Cargo.toml new file mode 100644 index 0000000..b14910e --- /dev/null +++ b/plugins/updater/Cargo.toml @@ -0,0 +1,86 @@ +[package] +name = "tauri-plugin-updater" +version = "2.12.0" +description = "In-app updates for Tauri applications." +edition = { workspace = true } +authors = { workspace = true } +license = { workspace = true } +rust-version = { workspace = true } +repository = { workspace = true } +links = "tauri-plugin-updater" +exclude = [ + "/banner.png", + "/guest-js", + "/package.json", + "/rollup.config.js", + "/tests", + "/tsconfig.json", +] + +[package.metadata.docs.rs] +no-default-features = true +features = ["zip"] + +[package.metadata.platforms.support] +windows = { level = "full", notes = "" } +linux = { level = "full", notes = "" } +macos = { level = "full", notes = "" } +android = { level = "none", notes = "" } +ios = { level = "none", notes = "" } + +[build-dependencies] +tauri-plugin = { workspace = true, features = ["build"] } + +[dev-dependencies] +tauri = { workspace = true, features = ["wry"] } + +[dependencies] +tauri = { workspace = true } +serde = { workspace = true } +serde_json = { workspace = true } +thiserror = { workspace = true } +log = { workspace = true } +tokio = "1" +reqwest = { version = "0.13", default-features = false, features = [ + "json", + "stream", +] } +rustls = { version = "0.23", default-features = false, features = [ + "ring", +], optional = true } +url = { workspace = true } +http = "1" +minisign-verify = "0.2" +time = { version = "0.3", features = ["parsing", "formatting"] } +base64 = "0.22" +semver = { version = "1", features = ["serde"] } +futures-util = "0.3" +tempfile = "3.20" +infer = "0.19" +percent-encoding = "2.3" + +[target."cfg(target_os = \"windows\")".dependencies] +zip = { version = "4", default-features = false, optional = true } +windows-sys = { version = "0.60.0", features = [ + "Win32_Foundation", + "Win32_UI_WindowsAndMessaging", + "Win32_UI_Shell", +] } + +[target."cfg(target_os = \"linux\")".dependencies] +dirs = "6" +tar = { version = "0.4", optional = true } +flate2 = { version = "1", optional = true } + +[target."cfg(target_os = \"macos\")".dependencies] +tar = "0.4" +flate2 = "1" +osakit = { version = "0.3", features = ["full"] } + +[features] +default = ["rustls-tls", "system-proxy", "zip"] +zip = ["dep:zip", "dep:tar", "dep:flate2"] +native-tls = ["reqwest/native-tls"] +native-tls-vendored = ["reqwest/native-tls-vendored"] +rustls-tls = ["reqwest/rustls-no-provider", "dep:rustls"] +system-proxy = ["reqwest/system-proxy"] diff --git a/plugins/updater/LICENSE.spdx b/plugins/updater/LICENSE.spdx new file mode 100644 index 0000000..cdd0df5 --- /dev/null +++ b/plugins/updater/LICENSE.spdx @@ -0,0 +1,20 @@ +SPDXVersion: SPDX-2.1 +DataLicense: CC0-1.0 +PackageName: tauri +DataFormat: SPDXRef-1 +PackageSupplier: Organization: The Tauri Programme in the Commons Conservancy +PackageHomePage: https://tauri.app +PackageLicenseDeclared: Apache-2.0 +PackageLicenseDeclared: MIT +PackageCopyrightText: 2019-2022, The Tauri Programme in the Commons Conservancy +PackageSummary: Tauri is a rust project that enables developers to make secure +and small desktop applications using a web frontend. + +PackageComment: The package includes the following libraries; see +Relationship information. + +Created: 2019-05-20T09:00:00Z +PackageDownloadLocation: git://github.com/tauri-apps/tauri +PackageDownloadLocation: git+https://github.com/tauri-apps/tauri.git +PackageDownloadLocation: git+ssh://github.com/tauri-apps/tauri.git +Creator: Person: Daniel Thompson-Yvetot \ No newline at end of file diff --git a/plugins/updater/LICENSE_APACHE-2.0 b/plugins/updater/LICENSE_APACHE-2.0 new file mode 100644 index 0000000..4947287 --- /dev/null +++ b/plugins/updater/LICENSE_APACHE-2.0 @@ -0,0 +1,177 @@ + + 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 \ No newline at end of file diff --git a/plugins/updater/LICENSE_MIT b/plugins/updater/LICENSE_MIT new file mode 100644 index 0000000..4d75472 --- /dev/null +++ b/plugins/updater/LICENSE_MIT @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2017 - Present Tauri Apps 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. \ No newline at end of file diff --git a/plugins/updater/README.md b/plugins/updater/README.md new file mode 100644 index 0000000..f122e74 --- /dev/null +++ b/plugins/updater/README.md @@ -0,0 +1,103 @@ +![plugin-updater](https://github.com/tauri-apps/plugins-workspace/raw/v2/plugins/updater/banner.png) + +In-app updates for Tauri applications. + +| Platform | Supported | +| -------- | --------- | +| Linux | ✓ | +| Windows | ✓ | +| macOS | ✓ | +| Android | x | +| iOS | x | + +## Install + +_This plugin requires a Rust version of at least **1.77.2**_ + +There are three general methods of installation that we can recommend. + +1. Use crates.io and npm (easiest, and requires you to trust that our publishing pipeline worked) +2. Pull sources directly from Github using git tags / revision hashes (most secure) +3. Git submodule install this repo in your tauri project and then use file protocol to ingest the source (most secure, but inconvenient to use) + +Install the Core plugin by adding the following to your `Cargo.toml` file: + +`src-tauri/Cargo.toml` + +```toml +# you can add the dependencies on the `[dependencies]` section if you do not target mobile +[target."cfg(not(any(target_os = \"android\", target_os = \"ios\")))".dependencies] +tauri-plugin-updater = "2.0.0" +# alternatively with Git: +tauri-plugin-updater = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v2" } +``` + +You can install the JavaScript Guest bindings using your preferred JavaScript package manager: + +```sh +pnpm add @tauri-apps/plugin-updater +# or +npm add @tauri-apps/plugin-updater +# or +yarn add @tauri-apps/plugin-updater +``` + +## Usage + +First you need to register the core plugin with Tauri: + +`src-tauri/src/lib.rs` + +```rust +fn main() { + tauri::Builder::default() + .setup(|app| { + #[cfg(desktop)] + app.handle().plugin(tauri_plugin_updater::Builder::new().build())?; + Ok(()) + }) + .run(tauri::generate_context!()) + .expect("error while running tauri application"); +} +``` + +Afterwards all the plugin's APIs are available through the JavaScript guest bindings: + +```javascript +import { check } from '@tauri-apps/plugin-updater' +import { relaunch } from '@tauri-apps/plugin-process' +const update = await check() +if (update) { + await update.downloadAndInstall() + // Relaunch the app on macOS and Linux to run the newly install version + await relaunch() +} +``` + +Note that for these APIs to work you have to properly configure the updater first and generate updater artifacts. Please refer to the [guide on our website](https://v2.tauri.app/plugin/updater/) for this. + +## Contributing + +PRs accepted. Please make sure to read the Contributing Guide before making a pull request. + +## Partners + + + + + + + +
+ + CrabNebula + +
+ +For the complete list of sponsors please visit our [website](https://tauri.app#sponsors) and [Open Collective](https://opencollective.com/tauri). + +## License + +Code: (c) 2015 - Present - The Tauri Programme within The Commons Conservancy. + +MIT or MIT/Apache 2.0 where applicable. diff --git a/plugins/updater/SECURITY.md b/plugins/updater/SECURITY.md new file mode 100644 index 0000000..4f09bba --- /dev/null +++ b/plugins/updater/SECURITY.md @@ -0,0 +1,23 @@ +# Security Policy + +**Do not report security vulnerabilities through public GitHub issues.** + +**Please use the [Private Vulnerability Disclosure](https://docs.github.com/en/code-security/security-advisories/guidance-on-reporting-and-writing-information-about-vulnerabilities/privately-reporting-a-security-vulnerability#privately-reporting-a-security-vulnerability) feature of GitHub.** + +Include as much of the following information: + +- Type of issue (e.g. improper input parsing, privilege escalation, etc.) +- The location of the affected source code (tag/branch/commit or direct URL) +- Any special configuration required to reproduce the issue +- The distribution affected or used to help us with reproduction of the issue +- Step-by-step instructions to reproduce the issue +- Ideally a reproduction repository +- Impact of the issue, including how an attacker might exploit the issue + +We prefer to receive reports in English. + +## Contact + +Please disclose a vulnerability or security relevant issue here: [https://github.com/tauri-apps/plugins-workspace/security/advisories/new](https://github.com/tauri-apps/plugins-workspace/security/advisories/new). + +Alternatively, you can also contact us by email via [security@tauri.app](mailto:security@tauri.app). diff --git a/plugins/updater/api-iife.js b/plugins/updater/api-iife.js new file mode 100644 index 0000000..242e6f0 --- /dev/null +++ b/plugins/updater/api-iife.js @@ -0,0 +1 @@ +if("__TAURI__"in window){var __TAURI_PLUGIN_UPDATER__=function(e,n){"use strict";class t extends n.Resource{constructor(e){super(e.rid),this.available=!0,this.currentVersion=e.currentVersion,this.version=e.version,this.date=e.date,this.body=e.body,this.rawJson=e.rawJson}async download(e,t){a(t);const s=new n.Channel;e&&(s.onmessage=e);const d=await n.invoke("plugin:updater|download",{onEvent:s,rid:this.rid,...t});this.downloadedBytes=new n.Resource(d)}async install(e){if(!this.downloadedBytes)throw new Error("Update.install called before Update.download");await n.invoke("plugin:updater|install",{updateRid:this.rid,bytesRid:this.downloadedBytes.rid,...e}),this.downloadedBytes=void 0}async downloadAndInstall(e,t){a(t);const s=new n.Channel;e&&(s.onmessage=e),await n.invoke("plugin:updater|download_and_install",{onEvent:s,rid:this.rid,...t})}async close(){await(this.downloadedBytes?.close()),await super.close()}}function a(e){e?.headers&&(e.headers=Array.from(new Headers(e.headers).entries()))}return e.Update=t,e.check=async function(e){a(e);const s=await n.invoke("plugin:updater|check",{...e});return s?new t(s):null},e}({},window.__TAURI__.core);Object.defineProperty(window.__TAURI__,"updater",{value:__TAURI_PLUGIN_UPDATER__})} diff --git a/plugins/updater/banner.png b/plugins/updater/banner.png new file mode 100644 index 0000000000000000000000000000000000000000..439110d842e2c80461003f4a9a5ede92ae410a96 GIT binary patch literal 57959 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
Sy5IL63lkgen*3W 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 literal 0 HcmV?d00001 diff --git a/plugins/updater/build.rs b/plugins/updater/build.rs new file mode 100644 index 0000000..30f70b9 --- /dev/null +++ b/plugins/updater/build.rs @@ -0,0 +1,25 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +const COMMANDS: &[&str] = &["check", "download", "install", "download_and_install"]; + +fn main() { + tauri_plugin::Builder::new(COMMANDS) + .global_api_script_path("./api-iife.js") + .build(); + + let target_os = std::env::var("CARGO_CFG_TARGET_OS").unwrap(); + let mobile = target_os == "ios" || target_os == "android"; + alias("desktop", !mobile); + alias("mobile", mobile); +} + +// creates a cfg alias if `has_feature` is true. +// `alias` must be a snake case string. +fn alias(alias: &str, has_feature: bool) { + println!("cargo:rustc-check-cfg=cfg({alias})"); + if has_feature { + println!("cargo:rustc-cfg={alias}"); + } +} diff --git a/plugins/updater/guest-js/index.ts b/plugins/updater/guest-js/index.ts new file mode 100644 index 0000000..4afbb0f --- /dev/null +++ b/plugins/updater/guest-js/index.ts @@ -0,0 +1,304 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +/** + * In-app updates for Tauri applications: check the configured endpoints for a new release, + * download it and install it. + * + * @module + */ + +import { invoke, Channel, Resource } from '@tauri-apps/api/core' + +/** Options used when checking for updates */ +interface CheckOptions { + /** + * The headers to send along with the update check request. + */ + headers?: HeadersInit + /** + * Timeout in milliseconds + */ + timeout?: number + /** + * A proxy url to be used when checking and downloading updates. + */ + proxy?: string + /** + * Target identifier for the running application. This is sent to the backend. + */ + target?: string +} + +/** Options used when downloading an update */ +interface DownloadOptions { + /** + * The headers to send along with the update download request. + */ + headers?: HeadersInit + /** + * Timeout in milliseconds + */ + timeout?: number +} + +/** Options used when installing an update */ +interface InstallOptions { + /** + * If the Windows installer should restart the app after installed, default is `true` + */ + restartAfterInstall?: boolean +} + +interface UpdateMetadata { + rid: number + currentVersion: string + version: string + date?: string + body?: string + rawJson: Record +} + +/** Updater download event */ +type DownloadEvent = + | { event: 'Started'; data: { contentLength?: number } } + | { event: 'Progress'; data: { chunkLength: number } } + | { event: 'Finished' } + +/** + * An update announced by the update server, as returned by {@linkcode check}. + * + * It holds a resource on the Rust side, so call {@linkcode Update.close} when you are done with it + * without installing it. + * + * @since 2.0.0 + */ +class Update extends Resource { + // TODO: remove this field in v3 + /** + * Whether an update is available. + * + * @deprecated This is always true, check if the return value is `null` instead when using {@linkcode check} + */ + available: boolean + /** + * The version of the application that is currently running. + */ + currentVersion: string + /** + * The version announced by the update server. + */ + version: string + /** + * The publish date of the update as an RFC 3339 string, when the server provided one. + */ + date?: string + /** + * The release notes of the update, when the server provided them. + */ + body?: string + /** + * The raw update manifest returned by the server, useful when it contains + * additional fields that the updater itself does not handle. + */ + rawJson: Record + private downloadedBytes?: Resource + + /** + * Creates an update from the metadata returned by the backend. + * You should not need to call this yourself, use {@linkcode check} instead. + * + * @param metadata The update information returned by the backend, including the resource identifier of the update. + * + * @example + * ```typescript + * import { check } from '@tauri-apps/plugin-updater'; + * // the update instance is created for you by `check` + * const update = await check(); + * ``` + */ + constructor(metadata: UpdateMetadata) { + super(metadata.rid) + this.available = true + this.currentVersion = metadata.currentVersion + this.version = metadata.version + this.date = metadata.date + this.body = metadata.body + this.rawJson = metadata.rawJson + } + + /** + * Downloads the updater package. Call {@linkcode install} later to install it. + * + * @example + * ```typescript + * import { check } from '@tauri-apps/plugin-updater'; + * + * const update = await check(); + * if (update) { + * let downloaded = 0; + * await update.download((event) => { + * if (event.event === 'Progress') { + * downloaded += event.data.chunkLength; + * console.log(`downloaded ${downloaded} bytes`); + * } + * }); + * await update.install(); + * } + * ``` + * + * @param onEvent Callback invoked with a `Started` event when the first chunk is received, a `Progress` event for every downloaded chunk and a `Finished` event when the download completes. + * @param options The headers and the timeout to use for the download request. + */ + async download( + onEvent?: (progress: DownloadEvent) => void, + options?: DownloadOptions + ): Promise { + convertToRustHeaders(options) + const channel = new Channel() + if (onEvent) { + channel.onmessage = onEvent + } + const downloadedBytesRid = await invoke('plugin:updater|download', { + onEvent: channel, + rid: this.rid, + ...options + }) + this.downloadedBytes = new Resource(downloadedBytesRid) + } + + /** + * Install downloaded updater package. Must be called after {@linkcode download}. + * + * ## Platform-specific: + * + * - **Windows:** This function exits the app after launching the updater installer successfully + * - **macOS / Linux:** You need to relaunch the app to run the newly install version + * + * @example + * ```typescript + * import { check } from '@tauri-apps/plugin-updater'; + * + * const update = await check(); + * if (update) { + * await update.download(); + * await update.install(); + * } + * ``` + * + * @param options Options for the installation, such as whether the Windows installer should restart the app afterwards. + */ + async install(options?: InstallOptions): Promise { + if (!this.downloadedBytes) { + throw new Error('Update.install called before Update.download') + } + + await invoke('plugin:updater|install', { + updateRid: this.rid, + bytesRid: this.downloadedBytes.rid, + ...options + }) + + // Don't need to call close, we did it in rust side already + this.downloadedBytes = undefined + } + + /** + * Downloads the updater package and installs it + * + * ## Platform-specific: + * + * - **Windows:** This function exits the app after launching the updater installer successfully + * - **macOS / Linux:** You need to relaunch the app to run the newly install version + * + * @example + * ```typescript + * import { check } from '@tauri-apps/plugin-updater'; + * + * const update = await check(); + * if (update) { + * await update.downloadAndInstall((event) => { + * console.log(event.event); + * }); + * } + * ``` + * + * @param onEvent Callback invoked with a `Started` event when the first chunk is received, a `Progress` event for every downloaded chunk and a `Finished` event when the download completes. + * @param options The headers and the timeout to use for the download request, and the installation options. + */ + async downloadAndInstall( + onEvent?: (progress: DownloadEvent) => void, + options?: DownloadOptions & InstallOptions + ): Promise { + convertToRustHeaders(options) + const channel = new Channel() + if (onEvent) { + channel.onmessage = onEvent + } + await invoke('plugin:updater|download_and_install', { + onEvent: channel, + rid: this.rid, + ...options + }) + } + + /** + * Releases the update resource and the downloaded bytes held by the backend. + * + * @example + * ```typescript + * import { check } from '@tauri-apps/plugin-updater'; + * + * const update = await check(); + * if (update) { + * await update.close(); + * } + * ``` + */ + async close(): Promise { + await this.downloadedBytes?.close() + await super.close() + } +} + +/** + * Checks the configured endpoints for an available update. + * + * @example + * ```typescript + * import { check } from '@tauri-apps/plugin-updater'; + * + * const update = await check(); + * if (update) { + * console.log(`update ${update.version} is available`); + * await update.downloadAndInstall(); + * } + * ``` + * + * @param options The headers, timeout, proxy and target to use for the update check request. + * + * @returns A promise resolving to the available {@linkcode Update}, or `null` when no update is available. + * + * @since 2.0.0 + */ +async function check(options?: CheckOptions): Promise { + convertToRustHeaders(options) + + const metadata = await invoke('plugin:updater|check', { + ...options + }) + return metadata ? new Update(metadata) : null +} + +/** + * Converts the headers in options to be an {@linkcode Array<[string, string]>} which is what the Rust side expects + */ +function convertToRustHeaders(options?: { headers?: HeadersInit }) { + if (options?.headers) { + options.headers = Array.from(new Headers(options.headers).entries()) + } +} + +export type { CheckOptions, DownloadOptions, DownloadEvent } +export { check, Update } diff --git a/plugins/updater/package.json b/plugins/updater/package.json new file mode 100644 index 0000000..820b3a1 --- /dev/null +++ b/plugins/updater/package.json @@ -0,0 +1,29 @@ +{ + "name": "@tauri-apps/plugin-updater", + "version": "2.12.0", + "license": "MIT OR Apache-2.0", + "authors": [ + "Tauri Programme within The Commons Conservancy" + ], + "repository": "https://github.com/tauri-apps/plugins-workspace", + "type": "module", + "types": "./dist-js/index.d.ts", + "main": "./dist-js/index.cjs", + "module": "./dist-js/index.js", + "exports": { + "types": "./dist-js/index.d.ts", + "import": "./dist-js/index.js", + "require": "./dist-js/index.cjs" + }, + "scripts": { + "build": "rollup -c" + }, + "files": [ + "dist-js", + "README.md", + "LICENSE" + ], + "dependencies": { + "@tauri-apps/api": "^2.11.0" + } +} diff --git a/plugins/updater/permissions/autogenerated/commands/check.toml b/plugins/updater/permissions/autogenerated/commands/check.toml new file mode 100644 index 0000000..fca73ce --- /dev/null +++ b/plugins/updater/permissions/autogenerated/commands/check.toml @@ -0,0 +1,13 @@ +# Automatically generated - DO NOT EDIT! + +"$schema" = "../../schemas/schema.json" + +[[permission]] +identifier = "allow-check" +description = "Enables the check command without any pre-configured scope." +commands.allow = ["check"] + +[[permission]] +identifier = "deny-check" +description = "Denies the check command without any pre-configured scope." +commands.deny = ["check"] diff --git a/plugins/updater/permissions/autogenerated/commands/download.toml b/plugins/updater/permissions/autogenerated/commands/download.toml new file mode 100644 index 0000000..896b30c --- /dev/null +++ b/plugins/updater/permissions/autogenerated/commands/download.toml @@ -0,0 +1,13 @@ +# Automatically generated - DO NOT EDIT! + +"$schema" = "../../schemas/schema.json" + +[[permission]] +identifier = "allow-download" +description = "Enables the download command without any pre-configured scope." +commands.allow = ["download"] + +[[permission]] +identifier = "deny-download" +description = "Denies the download command without any pre-configured scope." +commands.deny = ["download"] diff --git a/plugins/updater/permissions/autogenerated/commands/download_and_install.toml b/plugins/updater/permissions/autogenerated/commands/download_and_install.toml new file mode 100644 index 0000000..40858d1 --- /dev/null +++ b/plugins/updater/permissions/autogenerated/commands/download_and_install.toml @@ -0,0 +1,13 @@ +# Automatically generated - DO NOT EDIT! + +"$schema" = "../../schemas/schema.json" + +[[permission]] +identifier = "allow-download-and-install" +description = "Enables the download_and_install command without any pre-configured scope." +commands.allow = ["download_and_install"] + +[[permission]] +identifier = "deny-download-and-install" +description = "Denies the download_and_install command without any pre-configured scope." +commands.deny = ["download_and_install"] diff --git a/plugins/updater/permissions/autogenerated/commands/install.toml b/plugins/updater/permissions/autogenerated/commands/install.toml new file mode 100644 index 0000000..4c6a29d --- /dev/null +++ b/plugins/updater/permissions/autogenerated/commands/install.toml @@ -0,0 +1,13 @@ +# Automatically generated - DO NOT EDIT! + +"$schema" = "../../schemas/schema.json" + +[[permission]] +identifier = "allow-install" +description = "Enables the install command without any pre-configured scope." +commands.allow = ["install"] + +[[permission]] +identifier = "deny-install" +description = "Denies the install command without any pre-configured scope." +commands.deny = ["install"] diff --git a/plugins/updater/permissions/autogenerated/reference.md b/plugins/updater/permissions/autogenerated/reference.md new file mode 100644 index 0000000..938ca2e --- /dev/null +++ b/plugins/updater/permissions/autogenerated/reference.md @@ -0,0 +1,130 @@ +## Default Permission + +This permission set configures which kind of +updater functions are exposed to the frontend. + +#### Granted Permissions + +The full workflow from checking for updates to installing them +is enabled. + +#### This default permission set includes the following: + +- `allow-check` +- `allow-download` +- `allow-install` +- `allow-download-and-install` + +## Permission Table + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
IdentifierDescription
+ +`updater:allow-check` + + + +Enables the check command without any pre-configured scope. + +
+ +`updater:deny-check` + + + +Denies the check command without any pre-configured scope. + +
+ +`updater:allow-download` + + + +Enables the download command without any pre-configured scope. + +
+ +`updater:deny-download` + + + +Denies the download command without any pre-configured scope. + +
+ +`updater:allow-download-and-install` + + + +Enables the download_and_install command without any pre-configured scope. + +
+ +`updater:deny-download-and-install` + + + +Denies the download_and_install command without any pre-configured scope. + +
+ +`updater:allow-install` + + + +Enables the install command without any pre-configured scope. + +
+ +`updater:deny-install` + + + +Denies the install command without any pre-configured scope. + +
diff --git a/plugins/updater/permissions/default.toml b/plugins/updater/permissions/default.toml new file mode 100644 index 0000000..fcf08fa --- /dev/null +++ b/plugins/updater/permissions/default.toml @@ -0,0 +1,18 @@ +"$schema" = "schemas/schema.json" +[default] +description = """ +This permission set configures which kind of +updater functions are exposed to the frontend. + +#### Granted Permissions + +The full workflow from checking for updates to installing them +is enabled. + +""" +permissions = [ + "allow-check", + "allow-download", + "allow-install", + "allow-download-and-install", +] diff --git a/plugins/updater/permissions/schemas/schema.json b/plugins/updater/permissions/schemas/schema.json new file mode 100644 index 0000000..7f29591 --- /dev/null +++ b/plugins/updater/permissions/schemas/schema.json @@ -0,0 +1,354 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "PermissionFile", + "description": "Permission file that can define a default permission, a set of permissions or a list of inlined permissions.", + "type": "object", + "properties": { + "default": { + "description": "The default permission set for the plugin", + "anyOf": [ + { + "$ref": "#/definitions/DefaultPermission" + }, + { + "type": "null" + } + ] + }, + "set": { + "description": "A list of permissions sets defined", + "type": "array", + "items": { + "$ref": "#/definitions/PermissionSet" + } + }, + "permission": { + "description": "A list of inlined permissions", + "default": [], + "type": "array", + "items": { + "$ref": "#/definitions/Permission" + } + } + }, + "definitions": { + "DefaultPermission": { + "description": "The default permission set of the plugin.\n\nWorks similarly to a permission with the \"default\" identifier.", + "type": "object", + "required": [ + "permissions" + ], + "properties": { + "version": { + "description": "The version of the permission.", + "type": [ + "integer", + "null" + ], + "format": "uint64", + "minimum": 1.0 + }, + "description": { + "description": "Human-readable description of what the permission does. Tauri convention is to use `

` headings in markdown content for Tauri documentation generation purposes.", + "type": [ + "string", + "null" + ] + }, + "permissions": { + "description": "All permissions this set contains.", + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "PermissionSet": { + "description": "A set of direct permissions grouped together under a new name.", + "type": "object", + "required": [ + "description", + "identifier", + "permissions" + ], + "properties": { + "identifier": { + "description": "A unique identifier for the permission.", + "type": "string" + }, + "description": { + "description": "Human-readable description of what the permission does.", + "type": "string" + }, + "permissions": { + "description": "All permissions this set contains.", + "type": "array", + "items": { + "$ref": "#/definitions/PermissionKind" + } + } + } + }, + "Permission": { + "description": "Descriptions of explicit privileges of commands.\n\nIt can enable commands to be accessible in the frontend of the application.\n\nIf the scope is defined it can be used to fine grain control the access of individual or multiple commands.", + "type": "object", + "required": [ + "identifier" + ], + "properties": { + "version": { + "description": "The version of the permission.", + "type": [ + "integer", + "null" + ], + "format": "uint64", + "minimum": 1.0 + }, + "identifier": { + "description": "A unique identifier for the permission.", + "type": "string" + }, + "description": { + "description": "Human-readable description of what the permission does. Tauri internal convention is to use `

` headings in markdown content for Tauri documentation generation purposes.", + "type": [ + "string", + "null" + ] + }, + "commands": { + "description": "Allowed or denied commands when using this permission.", + "default": { + "allow": [], + "deny": [] + }, + "allOf": [ + { + "$ref": "#/definitions/Commands" + } + ] + }, + "scope": { + "description": "Allowed or denied scoped when using this permission.", + "allOf": [ + { + "$ref": "#/definitions/Scopes" + } + ] + }, + "platforms": { + "description": "Target platforms this permission applies. By default all platforms are affected by this permission.", + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/Target" + } + } + } + }, + "Commands": { + "description": "Allowed and denied commands inside a permission.\n\nIf two commands clash inside of `allow` and `deny`, it should be denied by default.", + "type": "object", + "properties": { + "allow": { + "description": "Allowed command.", + "default": [], + "type": "array", + "items": { + "type": "string" + } + }, + "deny": { + "description": "Denied command, which takes priority.", + "default": [], + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "Scopes": { + "description": "An argument for fine grained behavior control of Tauri commands.\n\nIt can be of any serde serializable type and is used to allow or prevent certain actions inside a Tauri command. The configured scope is passed to the command and will be enforced by the command implementation.\n\n## Example\n\n```json { \"allow\": [{ \"path\": \"$HOME/**\" }], \"deny\": [{ \"path\": \"$HOME/secret.txt\" }] } ```", + "type": "object", + "properties": { + "allow": { + "description": "Data that defines what is allowed by the scope.", + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/Value" + } + }, + "deny": { + "description": "Data that defines what is denied by the scope. This should be prioritized by validation logic.", + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/Value" + } + } + } + }, + "Value": { + "description": "All supported ACL values.", + "anyOf": [ + { + "description": "Represents a null JSON value.", + "type": "null" + }, + { + "description": "Represents a [`bool`].", + "type": "boolean" + }, + { + "description": "Represents a valid ACL [`Number`].", + "allOf": [ + { + "$ref": "#/definitions/Number" + } + ] + }, + { + "description": "Represents a [`String`].", + "type": "string" + }, + { + "description": "Represents a list of other [`Value`]s.", + "type": "array", + "items": { + "$ref": "#/definitions/Value" + } + }, + { + "description": "Represents a map of [`String`] keys to [`Value`]s.", + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/Value" + } + } + ] + }, + "Number": { + "description": "A valid ACL number.", + "anyOf": [ + { + "description": "Represents an [`i64`].", + "type": "integer", + "format": "int64" + }, + { + "description": "Represents a [`f64`].", + "type": "number", + "format": "double" + } + ] + }, + "Target": { + "description": "Platform target.", + "oneOf": [ + { + "description": "MacOS.", + "type": "string", + "enum": [ + "macOS" + ] + }, + { + "description": "Windows.", + "type": "string", + "enum": [ + "windows" + ] + }, + { + "description": "Linux.", + "type": "string", + "enum": [ + "linux" + ] + }, + { + "description": "Android.", + "type": "string", + "enum": [ + "android" + ] + }, + { + "description": "iOS.", + "type": "string", + "enum": [ + "iOS" + ] + } + ] + }, + "PermissionKind": { + "type": "string", + "oneOf": [ + { + "description": "Enables the check command without any pre-configured scope.", + "type": "string", + "const": "allow-check", + "markdownDescription": "Enables the check command without any pre-configured scope." + }, + { + "description": "Denies the check command without any pre-configured scope.", + "type": "string", + "const": "deny-check", + "markdownDescription": "Denies the check command without any pre-configured scope." + }, + { + "description": "Enables the download command without any pre-configured scope.", + "type": "string", + "const": "allow-download", + "markdownDescription": "Enables the download command without any pre-configured scope." + }, + { + "description": "Denies the download command without any pre-configured scope.", + "type": "string", + "const": "deny-download", + "markdownDescription": "Denies the download command without any pre-configured scope." + }, + { + "description": "Enables the download_and_install command without any pre-configured scope.", + "type": "string", + "const": "allow-download-and-install", + "markdownDescription": "Enables the download_and_install command without any pre-configured scope." + }, + { + "description": "Denies the download_and_install command without any pre-configured scope.", + "type": "string", + "const": "deny-download-and-install", + "markdownDescription": "Denies the download_and_install command without any pre-configured scope." + }, + { + "description": "Enables the install command without any pre-configured scope.", + "type": "string", + "const": "allow-install", + "markdownDescription": "Enables the install command without any pre-configured scope." + }, + { + "description": "Denies the install command without any pre-configured scope.", + "type": "string", + "const": "deny-install", + "markdownDescription": "Denies the install command without any pre-configured scope." + }, + { + "description": "This permission set configures which kind of\nupdater functions are exposed to the frontend.\n\n#### Granted Permissions\n\nThe full workflow from checking for updates to installing them\nis enabled.\n\n\n#### This default permission set includes:\n\n- `allow-check`\n- `allow-download`\n- `allow-install`\n- `allow-download-and-install`", + "type": "string", + "const": "default", + "markdownDescription": "This permission set configures which kind of\nupdater functions are exposed to the frontend.\n\n#### Granted Permissions\n\nThe full workflow from checking for updates to installing them\nis enabled.\n\n\n#### This default permission set includes:\n\n- `allow-check`\n- `allow-download`\n- `allow-install`\n- `allow-download-and-install`" + } + ] + } + } +} \ No newline at end of file diff --git a/plugins/updater/rollup.config.js b/plugins/updater/rollup.config.js new file mode 100644 index 0000000..1f349ec --- /dev/null +++ b/plugins/updater/rollup.config.js @@ -0,0 +1,7 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +import { createConfig } from '../../shared/rollup.config.js' + +export default createConfig() diff --git a/plugins/updater/src/commands.rs b/plugins/updater/src/commands.rs new file mode 100644 index 0000000..72684e6 --- /dev/null +++ b/plugins/updater/src/commands.rs @@ -0,0 +1,208 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use crate::{Result, Update, UpdaterExt}; + +use http::{HeaderMap, HeaderName, HeaderValue}; +use serde::Serialize; +use tauri::{ipc::Channel, Manager, Resource, ResourceId, Runtime, Webview}; + +use std::{str::FromStr, time::Duration}; +use url::Url; + +#[derive(Debug, Clone, Serialize)] +#[serde(tag = "event", content = "data")] +pub enum DownloadEvent { + #[serde(rename_all = "camelCase")] + Started { + content_length: Option, + }, + #[serde(rename_all = "camelCase")] + Progress { + chunk_length: usize, + }, + Finished, +} + +#[derive(Serialize, Default)] +#[serde(rename_all = "camelCase")] +pub(crate) struct Metadata { + rid: ResourceId, + current_version: String, + version: String, + date: Option, + body: Option, + raw_json: serde_json::Value, +} + +struct DownloadedBytes(pub Vec); +impl Resource for DownloadedBytes {} + +#[tauri::command] +pub(crate) async fn check( + webview: Webview, + headers: Option>, + timeout: Option, + proxy: Option, + target: Option, +) -> Result> { + let mut builder = webview.updater_builder(); + if let Some(headers) = headers { + for (k, v) in headers { + builder = builder.header(k, v)?; + } + } + if let Some(timeout) = timeout { + builder = builder.timeout(Duration::from_millis(timeout)); + } + if let Some(ref proxy) = proxy { + let url = Url::parse(proxy.as_str())?; + builder = builder.proxy(url); + } + if let Some(target) = target { + builder = builder.target(target); + } + + let updater = builder.build()?; + let update = updater.check().await?; + + if let Some(update) = update { + let formatted_date = if let Some(date) = update.date { + let formatted_date = date + .format(&time::format_description::well_known::Rfc3339) + .map_err(|_| crate::Error::FormatDate)?; + Some(formatted_date) + } else { + None + }; + let metadata = Metadata { + current_version: update.current_version.clone(), + version: update.version.clone(), + date: formatted_date, + body: update.body.clone(), + raw_json: update.raw_json.clone(), + rid: webview.resources_table().add(update), + }; + Ok(Some(metadata)) + } else { + Ok(None) + } +} + +#[tauri::command] +pub(crate) async fn download( + webview: Webview, + rid: ResourceId, + on_event: Channel, + headers: Option>, + timeout: Option, +) -> Result { + let update = webview.resources_table().get::(rid)?; + + let mut update = (*update).clone(); + + if let Some(headers) = headers { + let mut map = HeaderMap::new(); + for (k, v) in headers { + map.append(HeaderName::from_str(&k)?, HeaderValue::from_str(&v)?); + } + update.headers = map; + } + + if let Some(timeout) = timeout { + update.timeout = Some(Duration::from_millis(timeout)); + } + + let mut first_chunk = true; + let bytes = update + .download( + |chunk_length, content_length| { + if first_chunk { + first_chunk = !first_chunk; + let _ = on_event.send(DownloadEvent::Started { content_length }); + } + let _ = on_event.send(DownloadEvent::Progress { chunk_length }); + }, + || { + let _ = on_event.send(DownloadEvent::Finished); + }, + ) + .await?; + + Ok(webview.resources_table().add(DownloadedBytes(bytes))) +} + +#[tauri::command] +pub(crate) async fn install( + webview: Webview, + update_rid: ResourceId, + bytes_rid: ResourceId, + restart_after_install: Option, +) -> Result<()> { + let update = webview.resources_table().get::(update_rid)?; + let bytes = webview + .resources_table() + .get::(bytes_rid)?; + + if let Some(restart_after_install) = restart_after_install { + let update = (*update).clone(); + update + .restart_after_install(restart_after_install) + .install(&bytes.0)?; + } else { + update.install(&bytes.0)?; + } + + let _ = webview.resources_table().close(bytes_rid); + Ok(()) +} + +#[tauri::command] +pub(crate) async fn download_and_install( + webview: Webview, + rid: ResourceId, + on_event: Channel, + headers: Option>, + timeout: Option, + restart_after_install: Option, +) -> Result<()> { + let update = webview.resources_table().get::(rid)?; + + let mut update = (*update).clone(); + + if let Some(headers) = headers { + let mut map = HeaderMap::new(); + for (k, v) in headers { + map.append(HeaderName::from_str(&k)?, HeaderValue::from_str(&v)?); + } + update.headers = map; + } + + if let Some(timeout) = timeout { + update.timeout = Some(Duration::from_millis(timeout)); + } + + if let Some(restart_after_install) = restart_after_install { + update = update.restart_after_install(restart_after_install); + } + + let mut first_chunk = true; + + update + .download_and_install( + |chunk_length, content_length| { + if first_chunk { + first_chunk = !first_chunk; + let _ = on_event.send(DownloadEvent::Started { content_length }); + } + let _ = on_event.send(DownloadEvent::Progress { chunk_length }); + }, + || { + let _ = on_event.send(DownloadEvent::Finished); + }, + ) + .await?; + + Ok(()) +} diff --git a/plugins/updater/src/config.rs b/plugins/updater/src/config.rs new file mode 100644 index 0000000..9bd941a --- /dev/null +++ b/plugins/updater/src/config.rs @@ -0,0 +1,217 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use std::{ffi::OsString, fmt::Display}; + +use serde::{Deserialize, Deserializer}; +use url::Url; + +/// Install modes for the Windows update. +#[derive(Debug, PartialEq, Eq, Clone, Deserialize)] +#[serde(rename_all = "camelCase")] +#[derive(Default)] +pub enum WindowsUpdateInstallMode { + /// Specifies there's a basic UI during the installation process, including a final dialog box at the end. + BasicUi, + /// The quiet mode means there's no user interaction required. + /// Requires admin privileges if the installer does. + Quiet, + /// Specifies unattended mode, which means the installation only shows a progress bar. + #[default] + Passive, +} + +impl WindowsUpdateInstallMode { + /// Returns the associated `msiexec.exe` arguments. + pub fn msiexec_args(&self) -> &'static [&'static str] { + match self { + Self::BasicUi => &["/qb+"], + Self::Quiet => &["/quiet"], + Self::Passive => &["/passive"], + } + } + + #[cfg(windows)] + pub(crate) fn msi_restart_after_install_args(&self) -> &'static [&'static str] { + &["AUTOLAUNCHAPP=True"] + } + + /// Returns the associated nsis arguments. + pub fn nsis_args(&self) -> &'static [&'static str] { + // `/P`: Passive + // `/S`: Silent + // `/R`: Restart + match self { + Self::Passive => &["/P"], + Self::Quiet => &["/S"], + _ => &[], + } + } + + #[cfg(windows)] + pub(crate) fn nsis_restart_after_install_args(&self) -> &'static [&'static str] { + match self { + Self::BasicUi => &[], + _ => &["/R"], + } + } +} + +impl Display for WindowsUpdateInstallMode { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "{}", + match self { + Self::BasicUi => "basicUi", + Self::Quiet => "quiet", + Self::Passive => "passive", + } + ) + } +} + +#[derive(Debug, Clone, Deserialize, Default)] +#[serde(rename_all = "camelCase")] +pub struct WindowsConfig { + /// Additional arguments given to the NSIS or WiX installer. + /// + /// Note: this applies to both WiX and NSIS installers + #[serde( + default, + alias = "installer-args", + deserialize_with = "deserialize_os_string" + )] + pub installer_args: Vec, + /// Updating mode, defaults to `passive` mode. + /// + /// See [`WindowsUpdateInstallMode`] for more info. + #[serde(default, alias = "install-mode")] + pub install_mode: WindowsUpdateInstallMode, +} + +fn deserialize_os_string<'de, D>(deserializer: D) -> Result, D::Error> +where + D: Deserializer<'de>, +{ + Ok(Vec::::deserialize(deserializer)? + .into_iter() + .map(OsString::from) + .collect::>()) +} + +/// Updater configuration. +#[derive(Debug, Clone, Default)] +pub struct Config { + /// Dangerously allow using insecure transport protocols for update endpoints. + pub dangerous_insecure_transport_protocol: bool, + /// Dangerously accept invalid TLS certificates for update requests. + pub dangerous_accept_invalid_certs: bool, + /// Dangerously accept invalid hostnames for TLS certificates for update requests. + pub dangerous_accept_invalid_hostnames: bool, + /// Updater endpoints. + pub endpoints: Vec, + /// Signature public key. + pub pubkey: String, + /// Require the update signature to carry the version it was signed for, and reject the + /// update when that version differs from the one announced by the update endpoint. + /// + /// The endpoint response is fetched over TLS but is not itself signed, and the signature + /// only covers the downloaded artifact. Without this flag, anyone able to serve a crafted + /// response can pair an inflated `version` field with the `url` and `signature` of an + /// older release and force a downgrade to a genuine but outdated build, since that older + /// artifact carries a valid signature. + /// + /// The signed version is read from the signature's trusted comment, which is covered by + /// the signature. Releases signed before the Tauri CLI started recording it carry no + /// version, so enabling this rejects them. Re-sign and re-publish every release your users + /// can still update from before turning this on. + /// + /// This is checked independently of the version comparison: it constrains which artifact a + /// given version number may resolve to, not whether that version is newer. + /// + /// The default value of this flag is `false`. + pub require_signed_version: bool, + /// Allow the updater to install a release whose version is not newer than the + /// currently running one, changing the version check from "must be newer" to + /// "must be different". + /// + /// Note that the updater only verifies the signature of the downloaded artifact, + /// not the version advertised by the update endpoint, so enabling this removes the + /// only guard against installing a previously released (and validly signed) version. + /// + /// Ignored when the application sets a custom + /// [`Builder::default_version_comparator`](crate::Builder::default_version_comparator). + /// + /// The default value of this flag is `false`. + pub allow_downgrades: bool, + /// The Windows configuration for the updater. + pub windows: Option, +} + +impl<'de> Deserialize<'de> for Config { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + #[derive(Deserialize)] + #[serde(rename_all = "camelCase")] + pub struct Config { + #[serde(default, alias = "dangerous-insecure-transport-protocol")] + pub dangerous_insecure_transport_protocol: bool, + #[serde(default, alias = "dangerous-accept-invalid-certs")] + pub dangerous_accept_invalid_certs: bool, + #[serde(default, alias = "dangerous-accept-invalid-hostnames")] + pub dangerous_accept_invalid_hostnames: bool, + #[serde(default)] + pub endpoints: Vec, + pub pubkey: String, + #[serde(default, alias = "require-signed-version")] + pub require_signed_version: bool, + #[serde(default, alias = "allow-downgrades")] + pub allow_downgrades: bool, + pub windows: Option, + } + + let config = Config::deserialize(deserializer)?; + + validate_endpoints( + &config.endpoints, + config.dangerous_insecure_transport_protocol, + ) + .map_err(serde::de::Error::custom)?; + + Ok(Self { + dangerous_insecure_transport_protocol: config.dangerous_insecure_transport_protocol, + dangerous_accept_invalid_certs: config.dangerous_accept_invalid_certs, + dangerous_accept_invalid_hostnames: config.dangerous_accept_invalid_hostnames, + endpoints: config.endpoints, + pubkey: config.pubkey, + require_signed_version: config.require_signed_version, + allow_downgrades: config.allow_downgrades, + windows: config.windows, + }) + } +} + +pub(crate) fn validate_endpoints( + endpoints: &[Url], + dangerous_insecure_transport_protocol: bool, +) -> crate::Result<()> { + if !dangerous_insecure_transport_protocol { + for url in endpoints { + if url.scheme() != "https" { + #[cfg(debug_assertions)] + { + eprintln!("[\x1b[33mWARNING\x1b[0m] The updater endpoint \"{url}\" doesn't use `https` protocol. This is allowed in development but will fail in release builds."); + eprintln!("[\x1b[33mWARNING\x1b[0m] if this is a desired behavior, you can enable `dangerousInsecureTransportProtocol` in the plugin configuration"); + } + #[cfg(not(debug_assertions))] + return Err(crate::Error::InsecureTransportProtocol); + } + } + } + + Ok(()) +} diff --git a/plugins/updater/src/error.rs b/plugins/updater/src/error.rs new file mode 100644 index 0000000..916220b --- /dev/null +++ b/plugins/updater/src/error.rs @@ -0,0 +1,134 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use serde::{Serialize, Serializer}; +use thiserror::Error; + +/// All errors that can occur while running the updater. +#[derive(Debug, Error)] +#[non_exhaustive] +pub enum Error { + /// Endpoints are not sent. + #[error("Updater does not have any endpoints set.")] + EmptyEndpoints, + /// IO errors. + #[error(transparent)] + Io(#[from] std::io::Error), + /// Semver errors. + #[error(transparent)] + Semver(#[from] semver::Error), + /// Serialization errors. + #[error(transparent)] + Serialization(#[from] serde_json::Error), + /// Could not fetch a valid response from the server. + #[error("Could not fetch a valid release JSON from the remote")] + ReleaseNotFound, + /// Unsupported app architecture. + #[error("Unsupported application architecture, expected one of `x86`, `x86_64`, `arm` or `aarch64`.")] + UnsupportedArch, + /// Operating system is not supported. + #[error("Unsupported OS, expected one of `linux`, `darwin` or `windows`.")] + UnsupportedOs, + /// Failed to determine updater package extract path + #[error("Failed to determine updater package extract path.")] + FailedToDetermineExtractPath, + /// Url parsing errors. + #[error(transparent)] + UrlParse(#[from] url::ParseError), + /// `reqwest` crate errors. + #[error(transparent)] + Reqwest(#[from] reqwest::Error), + /// The platform was not found in the updater JSON response. + #[error("the platform `{0}` was not found in the response `platforms` object")] + TargetNotFound(String), + /// Neither the platform nor the fallback platform was found in the updater JSON response. + #[error( + "None of the fallback platforms `{0:?}` were found in the response `platforms` object" + )] + TargetsNotFound(Vec), + /// Download failed + #[error("`{0}`")] + Network(String), + /// `minisign_verify` errors. + #[error(transparent)] + Minisign(#[from] minisign_verify::Error), + /// `base64` errors. + #[error(transparent)] + Base64(#[from] base64::DecodeError), + /// UTF8 Errors in signature. + #[error("The signature {0} could not be decoded, please check if it is a valid base64 string. The signature must be the contents of the `.sig` file generated by the Tauri bundler, as a string.")] + SignatureUtf8(String), + #[cfg(all(target_os = "windows", feature = "zip"))] + /// `zip` errors. + #[error(transparent)] + Extract(#[from] zip::result::ZipError), + /// Temp dir is not on same mount mount. This prevents our updater to rename the AppImage to a temp file. + #[error("temp directory is not on the same mount point as the AppImage")] + TempDirNotOnSameMountPoint, + /// The downloaded archive does not contain a binary for the current target. + #[error("binary for the current target not found in the archive")] + BinaryNotFoundInArchive, + /// Could not create a temporary directory to store the downloaded update. + #[error("failed to create temporary directory")] + TempDirNotFound, + /// The privilege escalation prompt shown before installing a Linux package + /// failed or was dismissed by the user. + #[error("Authentication failed or was cancelled")] + AuthenticationFailed, + /// Installing the downloaded `.deb` package failed. + #[error("Failed to install .deb package")] + DebInstallFailed, + /// The package manager could not install the downloaded Linux package. + #[error("Failed to install package")] + PackageInstallFailed, + /// The downloaded update is not in a format the updater can install on the current platform. + #[error("invalid updater binary format")] + InvalidUpdaterFormat, + /// `http` crate errors. + #[error(transparent)] + Http(#[from] http::Error), + /// A request header value is not valid. + #[error(transparent)] + InvalidHeaderValue(#[from] http::header::InvalidHeaderValue), + /// A request header name is not valid. + #[error(transparent)] + InvalidHeaderName(#[from] http::header::InvalidHeaderName), + /// The release publish date could not be formatted as an RFC 3339 string. + #[error("Failed to format date")] + FormatDate, + /// The configured updater endpoint must use a secure protocol like `https` + #[error("The configured updater endpoint must use a secure protocol like `https`.")] + InsecureTransportProtocol, + /// The version the artifact was signed for does not match the version announced by the + /// update endpoint. + #[error( + "The update was signed for version {signed} but the update endpoint announced version {announced}. The endpoint response may have been tampered with to force installing a different release." + )] + SignedVersionMismatch { + /// The version read from the signature's trusted comment. + signed: String, + /// The version announced by the update endpoint. + announced: String, + }, + /// `requireSignedVersion` is enabled but the signature does not carry a version. + #[error( + "The update signature does not specify the version it was signed for, which `requireSignedVersion` requires. Re-sign and re-publish this release, or disable `requireSignedVersion`." + )] + MissingSignedVersion, + /// Tauri errors. + #[error(transparent)] + Tauri(#[from] tauri::Error), +} + +impl Serialize for Error { + fn serialize(&self, serializer: S) -> std::result::Result + where + S: Serializer, + { + serializer.serialize_str(self.to_string().as_ref()) + } +} + +/// A [`Result`](std::result::Result) alias whose error type is the updater [`Error`](enum@Error). +pub type Result = std::result::Result; diff --git a/plugins/updater/src/lib.rs b/plugins/updater/src/lib.rs new file mode 100644 index 0000000..8e58a40 --- /dev/null +++ b/plugins/updater/src/lib.rs @@ -0,0 +1,299 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +//! In-app updates for Tauri applications. +//! +//! Supported platforms: Windows, Linux and macOS. +//! +//! ## Cargo features +//! +//! - **zip** *(enabled by default)*: Adds support for compressed updater bundles from Tauri v1. +//! - **rustls-tls** *(enabled by default)*: Enables TLS functionality provided by `rustls`. +//! - **native-tls**: Enables TLS functionality provided by `native-tls`. +//! - **native-tls-vendored**: Enables the `vendored` feature of `native-tls`. +//! - **system-proxy** *(enabled by default)*: Use Windows and macOS system proxy settings automatically. + +#![doc( + html_logo_url = "https://github.com/tauri-apps/tauri/raw/dev/app-icon.png", + html_favicon_url = "https://github.com/tauri-apps/tauri/raw/dev/app-icon.png" +)] + +use std::{ffi::OsString, sync::Arc}; + +use http::{HeaderMap, HeaderName, HeaderValue}; +use semver::Version; +use tauri::{ + plugin::{Builder as PluginBuilder, TauriPlugin}, + Manager, Runtime, +}; + +mod commands; +mod config; +mod error; +mod updater; + +pub use config::Config; +pub use error::{Error, Result}; +pub use updater::*; + +/// Extensions to [`tauri::App`], [`tauri::AppHandle`], [`tauri::WebviewWindow`], [`tauri::Webview`] and [`tauri::Window`] to access the updater APIs. +pub trait UpdaterExt { + /// Gets the updater builder to build and updater + /// that can manually check if an update is available. + /// + /// # Examples + /// + /// ```no_run + /// use tauri_plugin_updater::UpdaterExt; + /// tauri::Builder::default() + /// .setup(|app| { + /// let handle = app.handle().clone(); + /// tauri::async_runtime::spawn(async move { + /// let response = handle.updater_builder().build().unwrap().check().await; + /// }); + /// Ok(()) + /// }); + /// ``` + fn updater_builder(&self) -> UpdaterBuilder; + + /// Gets the updater to manually check if an update is available. + /// + /// # Examples + /// + /// ```no_run + /// use tauri_plugin_updater::UpdaterExt; + /// tauri::Builder::default() + /// .setup(|app| { + /// let handle = app.handle().clone(); + /// tauri::async_runtime::spawn(async move { + /// let response = handle.updater().unwrap().check().await; + /// }); + /// Ok(()) + /// }); + /// ``` + fn updater(&self) -> Result; +} + +impl> UpdaterExt for T { + fn updater_builder(&self) -> UpdaterBuilder { + let app = self.app_handle(); + let UpdaterState { + config, + target, + version_comparator, + headers, + } = self.state::().inner(); + + let mut builder = UpdaterBuilder::new(app, config.clone()).headers(headers.clone()); + + if let Some(target) = target { + builder = builder.target(target); + } + + #[cfg(windows)] + { + builder = builder.current_exe_args(self.env().args_os); + } + + builder.version_comparator = version_comparator.clone(); + + // a comparator set by the application takes precedence over the configuration + if builder.version_comparator.is_none() && config.allow_downgrades { + builder = builder.version_comparator(|current, update| update.version != current); + } + + #[cfg(any( + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd" + ))] + { + let env = app.env(); + if let Some(appimage) = env.appimage { + builder = builder.executable_path(appimage); + } + } + + let app_handle = app.app_handle().clone(); + builder = builder.on_before_exit(move || { + app_handle.cleanup_before_exit(); + }); + + builder + } + + fn updater(&self) -> Result { + self.updater_builder().build() + } +} + +struct UpdaterState { + target: Option, + config: Config, + version_comparator: Option, + headers: HeaderMap, +} + +/// Builder for the updater plugin. +/// +/// The values set here are the defaults used by every [`Updater`] created through +/// [`UpdaterExt::updater`] and [`UpdaterExt::updater_builder`]; they can still be overridden +/// per updater instance on the [`UpdaterBuilder`]. +/// +/// # Examples +/// +/// ```no_run +/// use tauri::Runtime; +/// +/// fn register_updater(builder: tauri::Builder) -> tauri::Builder { +/// builder.plugin(tauri_plugin_updater::Builder::new().build()) +/// } +/// ``` +#[derive(Default)] +pub struct Builder { + target: Option, + pubkey: Option, + installer_args: Vec, + headers: HeaderMap, + default_version_comparator: Option, +} + +impl Builder { + /// Creates a new builder with the default configuration. + pub fn new() -> Self { + Self::default() + } + + /// Sets the target name used when checking for updates. + /// + /// It replaces the `{{target}}` variable in the endpoint URLs and is used as the key to look + /// up the release in the `platforms` object of a static update manifest. + /// + /// When it is not set, the updater uses the current operating system name (`linux`, `darwin` + /// or `windows`) in the endpoint URLs and looks for `{os}-{arch}-{bundle_type}` then + /// `{os}-{arch}` in the manifest. + pub fn target(mut self, target: impl Into) -> Self { + self.target.replace(target.into()); + self + } + + /// Sets the public key used to verify the update signature, + /// overriding the `pubkey` value of the plugin configuration. + pub fn pubkey>(mut self, pubkey: S) -> Self { + self.pubkey.replace(pubkey.into()); + self + } + + /// Adds an additional argument to pass to the Windows installer. + pub fn installer_args(mut self, args: I) -> Self + where + I: IntoIterator, + S: Into, + { + self.installer_args.extend(args.into_iter().map(Into::into)); + self + } + + /// Adds multiple additional arguments to pass to the Windows installer. + pub fn installer_arg(mut self, arg: S) -> Self + where + S: Into, + { + self.installer_args.push(arg.into()); + self + } + + /// Removes all the additional arguments to pass to the Windows installer. + /// + /// Note: this only removes the additional arguments added through [`Self::installer_args`], + /// not the ones managed by us (e.g. `/UPDATER` flag passed to the NSIS installer) + pub fn clear_installer_args(mut self) -> Self { + self.installer_args.clear(); + self + } + + /// Adds a header to be sent on every updater request. + /// + /// # Errors + /// + /// Returns an error if the header name or the header value is not valid. + pub fn header(mut self, key: K, value: V) -> Result + where + HeaderName: TryFrom, + >::Error: Into, + HeaderValue: TryFrom, + >::Error: Into, + { + let key: std::result::Result = key.try_into().map_err(Into::into); + let value: std::result::Result = + value.try_into().map_err(Into::into); + self.headers.insert(key?, value?); + + Ok(self) + } + + /// Replaces all the headers sent on updater requests with the given map, + /// discarding the ones previously added with [`Self::header`]. + pub fn headers(mut self, headers: HeaderMap) -> Self { + self.headers = headers; + self + } + + /// Sets the default function used to decide whether a remote release should be installed. + /// + /// The closure receives the current application version and the remote release, + /// and must return `true` when the release should be treated as an update. + /// + /// It applies to every updater created through [`UpdaterExt`] and takes precedence over the + /// `allowDowngrades` configuration value; it can still be overridden per updater instance with + /// [`UpdaterBuilder::version_comparator`]. When no comparator is set at all, a release is + /// installed only if its version is greater than the current one. + pub fn default_version_comparator< + F: Fn(Version, RemoteRelease) -> bool + Send + Sync + 'static, + >( + mut self, + f: F, + ) -> Self { + self.default_version_comparator.replace(Arc::new(f)); + self + } + + /// Builds the updater plugin, registering the `check`, `download`, `install` + /// and `download_and_install` commands used by the JavaScript API. + /// + /// Pass the returned plugin to [`tauri::Builder::plugin`]. + pub fn build(self) -> TauriPlugin { + let pubkey = self.pubkey; + let target = self.target; + let version_comparator = self.default_version_comparator; + let installer_args = self.installer_args; + let headers = self.headers; + PluginBuilder::::new("updater") + .setup(move |app, api| { + let mut config = api.config().clone(); + if let Some(pubkey) = pubkey { + config.pubkey = pubkey; + } + if let Some(windows) = &mut config.windows { + windows.installer_args.extend(installer_args); + } + app.manage(UpdaterState { + target, + config, + version_comparator, + headers, + }); + Ok(()) + }) + .invoke_handler(tauri::generate_handler![ + commands::check, + commands::download, + commands::install, + commands::download_and_install, + ]) + .build() + } +} diff --git a/plugins/updater/src/updater.rs b/plugins/updater/src/updater.rs new file mode 100644 index 0000000..d04a19c --- /dev/null +++ b/plugins/updater/src/updater.rs @@ -0,0 +1,1980 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use std::{ + collections::HashMap, + ffi::OsString, + io::Cursor, + path::{Path, PathBuf}, + str::FromStr, + sync::Arc, + time::Duration, +}; + +#[cfg(not(target_os = "macos"))] +use std::ffi::OsStr; + +use base64::Engine; +use futures_util::StreamExt; +use http::{header::ACCEPT, HeaderName}; +use minisign_verify::{PublicKey, Signature}; +use percent_encoding::{AsciiSet, CONTROLS}; +use reqwest::{ + header::{HeaderMap, HeaderValue}, + ClientBuilder, StatusCode, +}; +use semver::Version; +use serde::{de::Error as DeError, Deserialize, Deserializer, Serialize}; +use tauri::{ + utils::{ + config::BundleType, + platform::{bundle_type, current_exe}, + }, + AppHandle, Resource, Runtime, +}; +use time::OffsetDateTime; +use url::Url; + +use crate::{ + error::{Error, Result}, + Config, +}; + +const UPDATER_USER_AGENT: &str = concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION"),); + +/// The kind of bundle the running application was installed from. +/// +/// Its name is appended to the updater target string (`{os}-{arch}-{bundle_type}`) when looking +/// up the release in the update manifest and replaces the `{{bundle_type}}` variable in the +/// endpoint URLs. +#[derive(Copy, Clone)] +pub enum Installer { + /// Linux AppImage bundle, named `appimage`. + AppImage, + /// Debian package, named `deb`. + Deb, + /// RPM package, named `rpm`. + Rpm, + + /// macOS application bundle, named `app`. Also used for applications distributed as DMG. + App, + + /// Windows WiX (MSI) installer, named `msi`. + Msi, + /// Windows NSIS installer, named `nsis`. + Nsis, +} + +impl Installer { + fn name(self) -> &'static str { + match self { + Self::AppImage => "appimage", + Self::Deb => "deb", + Self::Rpm => "rpm", + Self::App => "app", + Self::Msi => "msi", + Self::Nsis => "nsis", + } + } +} + +/// The update information of a single platform in the update manifest. +#[derive(Debug, Deserialize, Serialize, Clone)] +pub struct ReleaseManifestPlatform { + /// Download URL for the platform + pub url: Url, + /// Signature for the platform + pub signature: String, +} + +/// The platform specific data of a [`RemoteRelease`], in either of the two supported shapes. +#[derive(Debug, Deserialize, Serialize, Clone)] +#[serde(untagged)] +pub enum RemoteReleaseInner { + /// Server Format: the endpoint resolved the platform itself and returned a single + /// download URL and signature. + Dynamic(ReleaseManifestPlatform), + /// Static Format: the manifest describes every platform it supports and the updater + /// picks the entry matching the current target. + Static { + /// Update information for each platform, keyed by the updater target string + /// (e.g. `darwin-aarch64`). + platforms: HashMap, + }, +} + +/// Information about a release returned by the remote update server. +/// +/// This type can have one of two shapes: Server Format (Dynamic Format) and Static Format. +#[derive(Debug, Clone)] +pub struct RemoteRelease { + /// Version to install. + pub version: Version, + /// Release notes. + pub notes: Option, + /// Release date. + pub pub_date: Option, + /// Release data. + pub data: RemoteReleaseInner, +} + +impl RemoteRelease { + /// The release's download URL for the given target. + pub fn download_url(&self, target: &str) -> Result<&Url> { + match self.data { + RemoteReleaseInner::Dynamic(ref platform) => Ok(&platform.url), + RemoteReleaseInner::Static { ref platforms } => platforms + .get(target) + .map_or(Err(Error::TargetNotFound(target.to_string())), |p| { + Ok(&p.url) + }), + } + } + + /// The release's signature for the given target. + pub fn signature(&self, target: &str) -> Result<&String> { + match self.data { + RemoteReleaseInner::Dynamic(ref platform) => Ok(&platform.signature), + RemoteReleaseInner::Static { ref platforms } => platforms + .get(target) + .map_or(Err(Error::TargetNotFound(target.to_string())), |platform| { + Ok(&platform.signature) + }), + } + } +} + +/// Function executed right before the Windows installer is spawned and the app exits. +/// See [`UpdaterBuilder::on_before_exit`]. +pub type OnBeforeExit = Arc; +/// Function that customizes the `reqwest` client builder used for the updater requests. +/// See [`UpdaterBuilder::configure_client`]. +pub type OnBeforeRequest = Arc ClientBuilder + Send + Sync + 'static>; +/// Function that decides whether a remote release must be installed. +/// +/// It receives the current application version and the remote release, +/// and returns `true` when the release should be treated as an update. +pub type VersionComparator = Arc bool + Send + Sync>; +#[cfg(target_os = "macos")] +type MainThreadClosure = Box; +#[cfg(target_os = "macos")] +type RunOnMainThread = Arc tauri::Result<()> + Send + Sync + 'static>; + +// TODO: Move more fields to this in v3 if we can mark those fields non `pub` +/// Updater context shared between [`UpdaterBuilder`], [`Updater`] and [`Update`] +#[derive(Clone)] +struct UpdaterContext { + config: Config, + configure_client: Option, + #[cfg(target_os = "macos")] + run_on_main_thread: RunOnMainThread, + /// App name, used for creating named tempfiles + #[cfg(windows)] + app_name: String, + #[cfg(windows)] + installer_args: Vec, + #[cfg(windows)] + current_exe_args: Vec, + #[cfg(windows)] + on_before_exit: Option, + #[cfg(windows)] + restart_after_install: bool, +} + +/// Builder for an [`Updater`] instance. +/// +/// Get one from [`crate::UpdaterExt::updater_builder`], which pre-fills it with the plugin +/// configuration, then call [`UpdaterBuilder::build`]. +pub struct UpdaterBuilder { + current_version: Version, + pub(crate) version_comparator: Option, + executable_path: Option, + target: Option, + endpoints: Option>, + headers: HeaderMap, + timeout: Option, + proxy: Option, + no_proxy: bool, + context: UpdaterContext, +} + +impl UpdaterBuilder { + pub(crate) fn new(app: &AppHandle, config: crate::Config) -> Self { + #[cfg(target_os = "macos")] + let run_on_main_thread = { + let app_ = app.clone(); + Arc::new(move |f| app_.run_on_main_thread(f)) + }; + Self { + context: UpdaterContext { + #[cfg(windows)] + installer_args: config + .windows + .as_ref() + .map(|w| w.installer_args.clone()) + .unwrap_or_default(), + config, + configure_client: None, + #[cfg(target_os = "macos")] + run_on_main_thread, + #[cfg(windows)] + app_name: app.package_info().name.clone(), + #[cfg(windows)] + current_exe_args: Vec::new(), + #[cfg(windows)] + on_before_exit: None, + #[cfg(windows)] + restart_after_install: true, + }, + current_version: app.package_info().version.clone(), + version_comparator: None, + executable_path: None, + target: None, + endpoints: None, + headers: Default::default(), + timeout: None, + proxy: None, + no_proxy: false, + } + } + + /// Sets the function used to decide whether the remote release must be installed, + /// replacing the comparator set with [`crate::Builder::default_version_comparator`] + /// and the behavior of the `allowDowngrades` configuration value. + /// + /// When no comparator is set, a release is only installed if its version is greater + /// than the current application version. + pub fn version_comparator bool + Send + Sync + 'static>( + mut self, + f: F, + ) -> Self { + self.version_comparator = Some(Arc::new(f)); + self + } + + /// Sets the target name used when checking for updates. + /// + /// It replaces the `{{target}}` variable in the endpoint URLs and is used as the key to look + /// up the release in the `platforms` object of a static update manifest. + /// + /// When it is not set, the updater uses the current operating system name (`linux`, `darwin` + /// or `windows`) in the endpoint URLs and looks for `{os}-{arch}-{bundle_type}` then + /// `{os}-{arch}` in the manifest. + pub fn target(mut self, target: impl Into) -> Self { + self.target.replace(target.into()); + self + } + + /// Sets the endpoints to fetch the update manifest from, + /// overriding the `endpoints` configuration value. + /// + /// They are checked in order and the first one that returns a valid release wins. + /// + /// # Errors + /// + /// Returns [`Error::InsecureTransportProtocol`] on release builds if an endpoint does not use + /// the `https` protocol and the `dangerousInsecureTransportProtocol` configuration value is + /// not enabled. On debug builds a warning is printed instead. + pub fn endpoints(mut self, endpoints: Vec) -> Result { + crate::config::validate_endpoints( + &endpoints, + self.context.config.dangerous_insecure_transport_protocol, + )?; + + self.endpoints.replace(endpoints); + Ok(self) + } + + /// Sets the path of the application executable, which is used to determine where the update + /// must be installed. Defaults to the path of the current executable, or to the AppImage path + /// when the application runs as an AppImage. + pub fn executable_path>(mut self, p: P) -> Self { + self.executable_path.replace(p.as_ref().into()); + self + } + + /// Adds a header to be sent on the update check and download requests. + /// + /// # Errors + /// + /// Returns an error if the header name or the header value is not valid. + pub fn header(mut self, key: K, value: V) -> Result + where + HeaderName: TryFrom, + >::Error: Into, + HeaderValue: TryFrom, + >::Error: Into, + { + let key: std::result::Result = key.try_into().map_err(Into::into); + let value: std::result::Result = + value.try_into().map_err(Into::into); + self.headers.insert(key?, value?); + + Ok(self) + } + + /// Replaces all the headers sent on the update check and download requests with the given map, + /// discarding the ones previously added with [`Self::header`]. + pub fn headers(mut self, headers: HeaderMap) -> Self { + self.headers = headers; + self + } + + /// Removes all the headers previously set on this builder. + pub fn clear_headers(mut self) -> Self { + self.headers.clear(); + self + } + + /// Sets the timeout of the update check and download requests. + /// When it is not set, the requests do not time out. + pub fn timeout(mut self, timeout: Duration) -> Self { + self.timeout = Some(timeout); + self + } + + /// Sets the proxy used for the update check and download requests. + /// It is ignored when [`Self::no_proxy`] was called. + pub fn proxy(mut self, proxy: Url) -> Self { + self.proxy.replace(proxy); + self + } + + /// Clear all proxies. See [`reqwest::ClientBuilder::no_proxy`](https://docs.rs/reqwest/latest/reqwest/struct.ClientBuilder.html#method.no_proxy). + pub fn no_proxy(mut self) -> Self { + self.no_proxy = true; + self + } + + /// Sets the public key used to verify the update signature, + /// overriding the `pubkey` value of the plugin configuration. + pub fn pubkey>(mut self, pubkey: S) -> Self { + self.context.config.pubkey = pubkey.into(); + self + } + + /// Adds an argument to pass to the Windows installer. + /// + /// Note: this applies to both WiX and NSIS installers + #[cfg_attr(not(windows), allow(unused))] + pub fn installer_arg(mut self, arg: S) -> Self + where + S: Into, + { + #[cfg(windows)] + { + self.context.installer_args.push(arg.into()); + } + self + } + + /// Adds multiple arguments to pass to the Windows installer. + /// + /// Note: this applies to both WiX and NSIS installers + #[cfg_attr(not(windows), allow(unused))] + pub fn installer_args(mut self, args: I) -> Self + where + I: IntoIterator, + S: Into, + { + #[cfg(windows)] + { + self.context + .installer_args + .extend(args.into_iter().map(Into::into)); + } + self + } + + /// Removes all the additional arguments to pass to the Windows installer. + /// + /// Note: this only removes the additional arguments added through + /// [`Self::installer_arg`], [`crate::Builder::installer_arg`] + /// and the `plugins > updater > windows > installerArgs` config, + /// not the ones managed by us (e.g. `/UPDATER` flag passed to the NSIS installer) + #[cfg_attr(not(windows), allow(unused))] + pub fn clear_installer_args(mut self) -> Self { + #[cfg(windows)] + { + self.context.installer_args.clear(); + } + self + } + + /// Function to run before we run the installer and exit the app through `std::process::exit(0)` on Windows + #[cfg_attr(not(windows), allow(unused))] + pub fn on_before_exit(mut self, f: F) -> Self { + #[cfg(windows)] + { + self.context.on_before_exit.replace(Arc::new(f)); + } + self + } + + /// If the Windows installer should restart the app after installed, default is `true` + #[cfg_attr(not(windows), allow(unused))] + pub fn restart_after_install(mut self, restart_after_install: bool) -> Self { + #[cfg(windows)] + { + self.context.restart_after_install = restart_after_install; + } + self + } + + /// Allows you to modify the `reqwest` client builder before the HTTP request is sent. + /// + /// Note that `reqwest` crate may be updated in minor releases of tauri-plugin-updater. + /// Therefore it's recommended to pin the plugin to at least a minor version when you're using `configure_client`. + pub fn configure_client ClientBuilder + Send + Sync + 'static>( + mut self, + f: F, + ) -> Self { + self.context.configure_client.replace(Arc::new(f)); + self + } + + /// Builds the [`Updater`]. + /// + /// # Errors + /// + /// - [`Error::EmptyEndpoints`]: neither [`Self::endpoints`] nor the `endpoints` + /// configuration value provided an endpoint to check. + /// - [`Error::UnsupportedArch`]: the updater does not support the current architecture. + /// - [`Error::FailedToDetermineExtractPath`]: the install directory could not be resolved + /// from the executable path. + pub fn build(self) -> Result { + let endpoints = self + .endpoints + .unwrap_or_else(|| self.context.config.endpoints.clone()); + + if endpoints.is_empty() { + return Err(Error::EmptyEndpoints); + }; + + let arch = updater_arch().ok_or(Error::UnsupportedArch)?; + + let executable_path = self.executable_path.clone().unwrap_or(current_exe()?); + + // Get the extract_path from the provided executable_path + let extract_path = if cfg!(target_os = "linux") { + executable_path + } else { + extract_path_from_executable(&executable_path)? + }; + + Ok(Updater { + current_version: self.current_version, + version_comparator: self.version_comparator, + timeout: self.timeout, + proxy: self.proxy, + no_proxy: self.no_proxy, + endpoints, + arch, + target: self.target, + headers: self.headers, + extract_path, + context: self.context.clone(), + }) + } +} + +#[cfg(windows)] +impl UpdaterBuilder { + pub(crate) fn current_exe_args(mut self, args: I) -> Self + where + I: IntoIterator, + S: Into, + { + self.context + .current_exe_args + .extend(args.into_iter().map(Into::into)); + self + } +} + +/// Checks the configured endpoints for an application update. +/// +/// Get one from [`crate::UpdaterExt::updater`] or by calling [`UpdaterBuilder::build`]. +pub struct Updater { + current_version: Version, + version_comparator: Option, + timeout: Option, + proxy: Option, + no_proxy: bool, + endpoints: Vec, + arch: &'static str, + // The `{{target}}` variable we replace in the endpoint and serach for in the JSON, + // this is either the user provided target or the current operating system by default + target: Option, + headers: HeaderMap, + extract_path: PathBuf, + context: UpdaterContext, +} + +impl Updater { + /// Checks the endpoints for an update, returning the first release that the version + /// comparator accepts. + /// + /// Each endpoint is requested in order, with the `{{current_version}}`, `{{target}}`, + /// `{{arch}}` and `{{bundle_type}}` variables replaced in its URL, until one of them + /// answers with a release manifest the updater can parse. + /// + /// Resolves to `None` when an endpoint replies with `204 No Content` or when the release it + /// announced is not considered an update - by default when its version is not greater than the + /// current application version. + /// + /// # Errors + /// + /// - [`Error::UnsupportedOs`]: no target was set and the updater does not support the + /// current operating system. + /// - [`Error::ReleaseNotFound`]: no endpoint returned a release manifest. + /// - The last request or deserialization error when every endpoint failed. + /// - [`Error::TargetNotFound`] or [`Error::TargetsNotFound`]: the manifest has no entry + /// for the current target. + pub async fn check(&self) -> Result> { + // we want JSON only + let mut headers = self.headers.clone(); + if !headers.contains_key(ACCEPT) { + headers.insert(ACCEPT, HeaderValue::from_static("application/json")); + } + + // Set SSL certs for linux if they aren't available. + #[cfg(target_os = "linux")] + { + if std::env::var_os("SSL_CERT_FILE").is_none() { + std::env::set_var("SSL_CERT_FILE", "/etc/ssl/certs/ca-certificates.crt"); + } + if std::env::var_os("SSL_CERT_DIR").is_none() { + std::env::set_var("SSL_CERT_DIR", "/etc/ssl/certs"); + } + } + let target = if let Some(target) = &self.target { + target + } else { + updater_os().ok_or(Error::UnsupportedOs)? + }; + + let mut remote_release: Option = None; + let mut raw_json: Option = None; + let mut last_error: Option = None; + for url in &self.endpoints { + // replace {{current_version}}, {{target}}, {{arch}} and {{bundle_type}} in the provided URL + // this is useful if we need to query example + // https://releases.myapp.com/update/{{target}}/{{arch}}/{{current_version}} + // will be translated into -> + // https://releases.myapp.com/update/darwin/aarch64/1.0.0 + // The main objective is if the update URL is defined via the Cargo.toml + // the URL will be generated dynamically + let version = self.current_version.to_string(); + let version = version.as_bytes(); + const CONTROLS_ADD: &AsciiSet = &CONTROLS.add(b'+'); + let encoded_version = percent_encoding::percent_encode(version, CONTROLS_ADD); + let encoded_version = encoded_version.to_string(); + let installer = installer_for_bundle_type(bundle_type()) + .map(|i| i.name()) + .unwrap_or("unknown"); + + let url: Url = url + .to_string() + // url::Url automatically url-encodes the path components + .replace("%7B%7Bcurrent_version%7D%7D", &encoded_version) + .replace("%7B%7Btarget%7D%7D", target) + .replace("%7B%7Barch%7D%7D", self.arch) + .replace("%7B%7Bbundle_type%7D%7D", installer) + // but not query parameters + .replace("{{current_version}}", &encoded_version) + .replace("{{target}}", target) + .replace("{{arch}}", self.arch) + .replace("{{bundle_type}}", installer) + .parse()?; + + log::debug!("checking for updates {url}"); + + #[cfg(feature = "rustls-tls")] + if rustls::crypto::CryptoProvider::get_default().is_none() { + // This can only fail if there is already a default provider which we checked for already. + let _ = rustls::crypto::ring::default_provider().install_default(); + } + + let mut request = ClientBuilder::new().user_agent(UPDATER_USER_AGENT); + if self.context.config.dangerous_accept_invalid_certs { + request = request.danger_accept_invalid_certs(true); + } + if self.context.config.dangerous_accept_invalid_hostnames { + request = request.danger_accept_invalid_hostnames(true); + } + if let Some(timeout) = self.timeout { + request = request.timeout(timeout); + } + if self.no_proxy { + log::debug!("disabling proxy"); + request = request.no_proxy(); + } else if let Some(ref proxy) = self.proxy { + log::debug!("using proxy {proxy}"); + let proxy = reqwest::Proxy::all(proxy.as_str())?; + request = request.proxy(proxy); + } + + if let Some(ref configure_client) = self.context.configure_client { + request = configure_client(request); + } + + let response = request + .build()? + .get(url) + .headers(headers.clone()) + .send() + .await; + + match response { + Ok(res) => { + if res.status().is_success() { + // no updates found! + if StatusCode::NO_CONTENT == res.status() { + log::debug!("update endpoint returned 204 No Content"); + return Ok(None); + }; + + let update_response: serde_json::Value = res.json().await?; + log::debug!("update response: {update_response:?}"); + raw_json = Some(update_response.clone()); + match serde_json::from_value::(update_response) + .map_err(Into::into) + { + Ok(release) => { + log::debug!("parsed release response {release:?}"); + last_error = None; + remote_release = Some(release); + // we found a release, break the loop + break; + } + Err(err) => { + log::error!("failed to deserialize update response: {err}"); + last_error = Some(err) + } + } + } else { + log::error!( + "update endpoint did not respond with a successful status code" + ); + } + } + Err(err) => { + log::error!("failed to check for updates: {err}"); + last_error = Some(err.into()) + } + } + } + + // Last error is cleaned on success. + // Shouldn't be triggered if we had a successfull call + if let Some(error) = last_error { + return Err(error); + } + + // Extracted remote metadata + let release = remote_release.ok_or(Error::ReleaseNotFound)?; + + let should_update = match self.version_comparator.as_ref() { + Some(comparator) => comparator(self.current_version.clone(), release.clone()), + None => release.version > self.current_version, + }; + + let installer = installer_for_bundle_type(bundle_type()); + let (download_url, signature) = self.get_urls(&release, &installer)?; + + let update = if should_update { + Some(Update { + current_version: self.current_version.to_string(), + target: target.to_owned(), + extract_path: self.extract_path.clone(), + version: release.version.to_string(), + date: release.pub_date, + download_url: download_url.clone(), + signature: signature.to_owned(), + body: release.notes, + raw_json: raw_json.unwrap(), + timeout: None, + proxy: self.proxy.clone(), + no_proxy: self.no_proxy, + headers: self.headers.clone(), + context: self.context.clone(), + }) + } else { + None + }; + + Ok(update) + } + + fn get_urls<'a>( + &self, + release: &'a RemoteRelease, + installer: &Option, + ) -> Result<(&'a Url, &'a String)> { + // Use the user provided target + if let Some(target) = &self.target { + return Ok((release.download_url(target)?, release.signature(target)?)); + } + + // Or else we search for [`{os}-{arch}-{installer}`, `{os}-{arch}`] in order + let os = updater_os().ok_or(Error::UnsupportedOs)?; + let arch = self.arch; + let mut targets = Vec::new(); + if let Some(installer) = installer { + let installer = installer.name(); + targets.push(format!("{os}-{arch}-{installer}")); + } + targets.push(format!("{os}-{arch}")); + + for target in &targets { + log::debug!("Searching for updater target '{target}' in release data"); + if let (Ok(download_url), Ok(signature)) = + (release.download_url(target), release.signature(target)) + { + return Ok((download_url, signature)); + }; + } + + Err(Error::TargetsNotFound(targets)) + } +} + +/// An update announced by the remote server, returned by [`Updater::check`]. +/// +/// Use [`Update::download`] followed by [`Update::install`], or [`Update::download_and_install`], +/// to apply it. +#[derive(Clone)] +pub struct Update { + /// Update description + pub body: Option, + /// Version used to check for update + pub current_version: String, + /// Version announced + pub version: String, + /// Update publish date + pub date: Option, + /// The `{{target}}` variable we replace in the endpoint and search for in the JSON, + /// this is either the user provided target or the current operating system by default + pub target: String, + /// Download URL announced + pub download_url: Url, + /// Signature announced + pub signature: String, + /// The raw version of server's JSON response. Useful if the response contains additional fields that the updater doesn't handle. + pub raw_json: serde_json::Value, + /// Request timeout + pub timeout: Option, + /// Request proxy + pub proxy: Option, + /// Disable system proxy + pub no_proxy: bool, + /// Request headers + pub headers: HeaderMap, + /// Extract path + #[allow(unused)] + extract_path: PathBuf, + context: UpdaterContext, +} + +impl Resource for Update {} + +impl Update { + /// Downloads the updater package, verifies it then return it as bytes. + /// + /// Use [`Update::install`] to install it + pub async fn download), D: FnOnce()>( + &self, + mut on_chunk: C, + on_download_finish: D, + ) -> Result> { + // set our headers + let mut headers = self.headers.clone(); + if !headers.contains_key(ACCEPT) { + headers.insert(ACCEPT, HeaderValue::from_static("application/octet-stream")); + } + + let mut request = ClientBuilder::new().user_agent(UPDATER_USER_AGENT); + if self.context.config.dangerous_accept_invalid_certs { + request = request.danger_accept_invalid_certs(true); + } + if self.context.config.dangerous_accept_invalid_hostnames { + request = request.danger_accept_invalid_hostnames(true); + } + if let Some(timeout) = self.timeout { + request = request.timeout(timeout); + } + if self.no_proxy { + request = request.no_proxy(); + } else if let Some(ref proxy) = self.proxy { + let proxy = reqwest::Proxy::all(proxy.as_str())?; + request = request.proxy(proxy); + } + if let Some(ref configure_client) = self.context.configure_client { + request = configure_client(request); + } + let response = request + .build()? + .get(self.download_url.clone()) + .headers(headers) + .send() + .await?; + + if !response.status().is_success() { + return Err(Error::Network(format!( + "Download request failed with status: {}", + response.status() + ))); + } + + let content_length: Option = response + .headers() + .get("Content-Length") + .and_then(|value| value.to_str().ok()) + .and_then(|value| value.parse().ok()); + + let mut buffer = Vec::new(); + + let mut stream = response.bytes_stream(); + while let Some(chunk) = stream.next().await { + let chunk = chunk?; + on_chunk(chunk.len(), content_length); + buffer.extend(chunk); + } + on_download_finish(); + + verify_signature( + &buffer, + &self.signature, + &self.context.config.pubkey, + &self.version, + self.context.config.require_signed_version, + )?; + + Ok(buffer) + } + + /// Installs the updater package downloaded by [`Update::download`] + /// + /// ## Platform-specific: + /// + /// - **Windows:** This function exits the app after launching the updater installer successfully + /// - **macOS / Linux:** You need to relaunch the app to run the newly install version + pub fn install(&self, bytes: impl AsRef<[u8]>) -> Result<()> { + self.install_inner(bytes.as_ref()) + } + + /// Downloads and installs the updater package + /// + /// ## Platform-specific: + /// + /// - **Windows:** This function exits the app after launching the updater installer successfully + /// - **macOS / Linux:** You need to relaunch the app to run the newly install version + pub async fn download_and_install), D: FnOnce()>( + &self, + on_chunk: C, + on_download_finish: D, + ) -> Result<()> { + let bytes = self.download(on_chunk, on_download_finish).await?; + self.install(bytes) + } + + #[cfg(mobile)] + fn install_inner(&self, _bytes: &[u8]) -> Result<()> { + Ok(()) + } + + /// Whether the Windows installer should restart the app after installed, default is `true` + #[cfg_attr(not(windows), allow(unused))] + pub fn restart_after_install(mut self, restart_after_install: bool) -> Self { + #[cfg(windows)] + { + self.context.restart_after_install = restart_after_install; + } + self + } +} + +#[cfg(windows)] +enum WindowsUpdaterType { + Nsis { + path: PathBuf, + #[allow(unused)] + temp: Option, + }, + Msi { + path: PathBuf, + #[allow(unused)] + temp: Option, + }, +} + +#[cfg(windows)] +impl WindowsUpdaterType { + fn nsis(path: PathBuf, temp: Option) -> Self { + Self::Nsis { path, temp } + } + + fn msi(path: PathBuf, temp: Option) -> Self { + Self::Msi { + path: path.wrap_in_quotes(), + temp, + } + } +} + +#[cfg(windows)] +impl Config { + fn install_mode(&self) -> crate::config::WindowsUpdateInstallMode { + self.windows + .as_ref() + .map(|w| w.install_mode.clone()) + .unwrap_or_default() + } +} + +/// Windows +#[cfg(windows)] +impl Update { + /// ### Expected structure: + /// ├── [AppName]_[version]_x64.msi # Application MSI + /// ├── [AppName]_[version]_x64-setup.exe # NSIS installer + /// ├── [AppName]_[version]_x64.msi.zip # ZIP generated by tauri-bundler + /// │ └──[AppName]_[version]_x64.msi # Application MSI + /// ├── [AppName]_[version]_x64-setup.exe.zip # ZIP generated by tauri-bundler + /// │ └──[AppName]_[version]_x64-setup.exe # NSIS installer + /// └── ... + fn install_inner(&self, bytes: &[u8]) -> Result<()> { + use windows_sys::{ + w, + Win32::UI::{Shell::ShellExecuteW, WindowsAndMessaging::SW_SHOW}, + }; + + let updater_type = self.extract(bytes)?; + + if let Some(on_before_exit) = self.context.on_before_exit.as_ref() { + log::debug!("running on_before_exit hook"); + on_before_exit(); + } + + let file = match &updater_type { + WindowsUpdaterType::Nsis { path, .. } => path.as_os_str().to_os_string(), + WindowsUpdaterType::Msi { .. } => std::env::var("SYSTEMROOT").as_ref().map_or_else( + |_| OsString::from("msiexec.exe"), + |p| OsString::from(format!("{p}\\System32\\msiexec.exe")), + ), + }; + let parameters = self.updater_parameters(&updater_type); + + log::debug!("Executing updater {file:?} with parameters: {parameters:?}"); + + let file = encode_wide(file); + let parameters = encode_wide(parameters); + + let result = unsafe { + ShellExecuteW( + std::ptr::null_mut(), + w!("open"), + file.as_ptr(), + parameters.as_ptr(), + std::ptr::null(), + SW_SHOW, + ) + }; + if result as isize <= 32 { + return Err(crate::Error::Io(std::io::Error::last_os_error())); + } + + std::process::exit(0); + } + + fn updater_parameters(&self, updater_type: &WindowsUpdaterType) -> OsString { + let install_mode = self.context.config.install_mode(); + let current_args = &self.context.current_exe_args[1..]; + + match updater_type { + WindowsUpdaterType::Nsis { .. } => { + let mut installer_args: Vec<&OsStr> = Vec::new(); + installer_args.extend(install_mode.nsis_args().iter().map(OsStr::new)); + installer_args.push(OsStr::new("/UPDATE")); + + let nsis_current_exe_arg; + if self.context.restart_after_install { + nsis_current_exe_arg = current_args + .iter() + .map(escape_nsis_current_exe_arg) + .collect::>(); + + installer_args.extend( + install_mode + .nsis_restart_after_install_args() + .iter() + .map(OsStr::new), + ); + installer_args.push(OsStr::new("/ARGS")); + installer_args.extend(nsis_current_exe_arg.iter().map(OsStr::new)); + } + + installer_args.extend(self.installer_args()); + + installer_args.join(OsStr::new(" ")) + } + WindowsUpdaterType::Msi { path, .. } => { + let mut installer_args: Vec<&OsStr> = vec![OsStr::new("/i"), path.as_os_str()]; + installer_args.extend(install_mode.msiexec_args().iter().map(OsStr::new)); + installer_args.push(OsStr::new("/promptrestart")); + installer_args.extend(self.installer_args()); + + let msi_current_exe_arg; + if self.context.restart_after_install { + msi_current_exe_arg = format!( + "LAUNCHAPPARGS=\"{}\"", + current_args + .iter() + .map(escape_msi_property_arg) + .collect::>() + .join(" ") + ); + + installer_args.extend( + install_mode + .msi_restart_after_install_args() + .iter() + .map(OsStr::new), + ); + installer_args.push(OsStr::new(&msi_current_exe_arg)); + } + + installer_args.join(OsStr::new(" ")) + } + } + } + + fn installer_args( + &self, + ) -> std::iter::Map, fn(&OsString) -> &OsStr> { + self.context.installer_args.iter().map(OsStr::new) + } + + fn extract(&self, bytes: &[u8]) -> Result { + #[cfg(feature = "zip")] + if infer::archive::is_zip(bytes) { + return self.extract_zip(bytes); + } + + self.extract_exe(bytes) + } + + fn make_temp_dir(&self) -> Result { + Ok(tempfile::Builder::new() + .prefix(&format!( + "{}-{}-updater-", + self.context.app_name, self.version + )) + .tempdir()? + .keep()) + } + + #[cfg(feature = "zip")] + fn extract_zip(&self, bytes: &[u8]) -> Result { + let temp_dir = self.make_temp_dir()?; + + let archive = Cursor::new(bytes); + let mut extractor = zip::ZipArchive::new(archive)?; + extractor.extract(&temp_dir)?; + + let paths = std::fs::read_dir(&temp_dir)?; + for path in paths { + let path = path?.path(); + let ext = path.extension(); + if ext == Some(OsStr::new("exe")) { + return Ok(WindowsUpdaterType::nsis(path, None)); + } else if ext == Some(OsStr::new("msi")) { + return Ok(WindowsUpdaterType::msi(path, None)); + } + } + + Err(crate::Error::BinaryNotFoundInArchive) + } + + fn extract_exe(&self, bytes: &[u8]) -> Result { + if infer::app::is_exe(bytes) { + let (path, temp) = self.write_to_temp(bytes, ".exe")?; + Ok(WindowsUpdaterType::nsis(path, temp)) + } else if infer::archive::is_msi(bytes) { + let (path, temp) = self.write_to_temp(bytes, ".msi")?; + Ok(WindowsUpdaterType::msi(path, temp)) + } else { + Err(crate::Error::InvalidUpdaterFormat) + } + } + + fn write_to_temp( + &self, + bytes: &[u8], + ext: &str, + ) -> Result<(PathBuf, Option)> { + use std::io::Write; + + let temp_dir = self.make_temp_dir()?; + let mut temp_file = tempfile::Builder::new() + .prefix(&format!( + "{}-{}-installer", + self.context.app_name, self.version + )) + .suffix(ext) + .rand_bytes(0) + .tempfile_in(temp_dir)?; + temp_file.write_all(bytes)?; + + let temp = temp_file.into_temp_path(); + Ok((temp.to_path_buf(), Some(temp))) + } +} + +/// Linux (AppImage, Deb, RPM) +#[cfg(any( + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd" +))] +impl Update { + /// ### Expected structure: + /// ├── [AppName]_[version]_amd64.AppImage.tar.gz # GZ generated by tauri-bundler + /// │ └──[AppName]_[version]_amd64.AppImage # Application AppImage + /// ├── [AppName]_[version]_amd64.deb # Debian package + /// ├── [AppName]_[version]_amd64.rpm # RPM package + /// └── ... + /// + fn install_inner(&self, bytes: &[u8]) -> Result<()> { + match installer_for_bundle_type(bundle_type()) { + Some(Installer::Deb) => self.install_deb(bytes), + Some(Installer::Rpm) => self.install_rpm(bytes), + _ => self.install_appimage(bytes), + } + } + + fn install_appimage(&self, bytes: &[u8]) -> Result<()> { + use std::os::unix::fs::{MetadataExt, PermissionsExt}; + let extract_path_metadata = self.extract_path.metadata()?; + + let tmp_dir_locations = vec![ + Box::new(|| Some(std::env::temp_dir())) as Box Option>, + Box::new(dirs::cache_dir), + Box::new(|| Some(self.extract_path.parent().unwrap().to_path_buf())), + ]; + + for tmp_dir_location in tmp_dir_locations { + if let Some(tmp_dir_location) = tmp_dir_location() { + let tmp_dir = tempfile::Builder::new() + .prefix("tauri_current_app") + .tempdir_in(tmp_dir_location)?; + let tmp_dir_metadata = tmp_dir.path().metadata()?; + + if extract_path_metadata.dev() == tmp_dir_metadata.dev() { + let mut perms = tmp_dir_metadata.permissions(); + perms.set_mode(0o700); + std::fs::set_permissions(tmp_dir.path(), perms)?; + + let tmp_app_image = &tmp_dir.path().join("current_app.AppImage"); + + let permissions = std::fs::metadata(&self.extract_path)?.permissions(); + + // create a backup of our current app image + std::fs::rename(&self.extract_path, tmp_app_image)?; + + #[cfg(feature = "zip")] + if infer::archive::is_gz(bytes) { + log::debug!("extracting AppImage"); + // extract the buffer to the tmp_dir + // we extract our signed archive into our final directory without any temp file + let archive = Cursor::new(bytes); + let decoder = flate2::read::GzDecoder::new(archive); + let mut archive = tar::Archive::new(decoder); + for mut entry in archive.entries()?.flatten() { + if let Ok(path) = entry.path() { + if path.extension() == Some(OsStr::new("AppImage")) { + // if something went wrong during the extraction, we should restore previous app + if let Err(err) = entry.unpack(&self.extract_path) { + std::fs::rename(tmp_app_image, &self.extract_path)?; + return Err(err.into()); + } + // early finish we have everything we need here + return Ok(()); + } + } + } + // if we have not returned early we should restore the backup + std::fs::rename(tmp_app_image, &self.extract_path)?; + return Err(Error::BinaryNotFoundInArchive); + } + + log::debug!("rewriting AppImage"); + return match std::fs::write(&self.extract_path, bytes) + .and_then(|_| std::fs::set_permissions(&self.extract_path, permissions)) + { + Err(err) => { + // if something went wrong during the extraction, we should restore previous app + std::fs::rename(tmp_app_image, &self.extract_path)?; + Err(err.into()) + } + Ok(_) => Ok(()), + }; + } + } + } + + Err(Error::TempDirNotOnSameMountPoint) + } + + fn install_deb(&self, bytes: &[u8]) -> Result<()> { + // First verify the bytes are actually a .deb package + if !infer::archive::is_deb(bytes) { + log::warn!("update is not a valid deb package"); + return Err(Error::InvalidUpdaterFormat); + } + + self.try_tmp_locations(bytes, "dpkg", "-i", "deb") + } + + fn install_rpm(&self, bytes: &[u8]) -> Result<()> { + // First verify the bytes are actually a .rpm package + if !infer::archive::is_rpm(bytes) { + return Err(Error::InvalidUpdaterFormat); + } + self.try_tmp_locations(bytes, "rpm", "-U", "rpm") + } + + fn try_tmp_locations( + &self, + bytes: &[u8], + install_cmd: &str, + install_arg: &str, + package_extension: &str, + ) -> Result<()> { + // Try different temp directories + let tmp_dir_locations = vec![ + Box::new(|| Some(std::env::temp_dir())) as Box Option>, + Box::new(dirs::cache_dir), + Box::new(|| Some(self.extract_path.parent().unwrap().to_path_buf())), + ]; + + // Try writing to multiple temp locations until one succeeds + for tmp_dir_location in tmp_dir_locations { + if let Some(path) = tmp_dir_location() { + let prefix = format!("tauri_{package_extension}_update"); + if let Ok(tmp_dir) = tempfile::Builder::new().prefix(&prefix).tempdir_in(path) { + let pkg_path = tmp_dir.path().join(format!("package.{package_extension}")); + + // Try writing the .deb / .rpm file + if std::fs::write(&pkg_path, bytes).is_ok() { + // If write succeeds, proceed with installation + return self.try_install_with_privileges( + &pkg_path, + install_cmd, + install_arg, + ); + } + // If write fails, continue to next temp location + } + } + } + + // If we get here, all temp locations failed + Err(Error::TempDirNotFound) + } + + fn try_install_with_privileges( + &self, + pkg_path: &Path, + install_cmd: &str, + install_arg: &str, + ) -> Result<()> { + // 1. First try using pkexec (graphical sudo prompt) + if let Ok(status) = std::process::Command::new("pkexec") + .arg(install_cmd) + .arg(install_arg) + .arg(pkg_path) + .status() + { + if status.success() { + log::debug!("installed {pkg_path:?} with pkexec"); + return Ok(()); + } + } + + // 2. Try zenity or kdialog for a graphical sudo experience + if let Ok(password) = self.get_password_graphically() { + if self.install_with_sudo(pkg_path, &password, install_cmd, install_arg)? { + log::debug!("installed {pkg_path:?} with GUI sudo"); + return Ok(()); + } + } + + // 3. Final fallback: terminal sudo + let status = std::process::Command::new("sudo") + .arg(install_cmd) + .arg(install_arg) + .arg(pkg_path) + .status()?; + + if status.success() { + log::debug!("installed {pkg_path:?} with sudo"); + Ok(()) + } else { + Err(Error::PackageInstallFailed) + } + } + + fn get_password_graphically(&self) -> Result { + // Try zenity first + let zenity_result = std::process::Command::new("zenity") + .args([ + "--password", + "--title=Authentication Required", + "--text=Enter your password to install the update:", + ]) + .output(); + + if let Ok(output) = zenity_result { + if output.status.success() { + return Ok(String::from_utf8_lossy(&output.stdout).trim().to_string()); + } + } + + // Fall back to kdialog if zenity fails or isn't available + let kdialog_result = std::process::Command::new("kdialog") + .args(["--password", "Enter your password to install the update:"]) + .output(); + + if let Ok(output) = kdialog_result { + if output.status.success() { + return Ok(String::from_utf8_lossy(&output.stdout).trim().to_string()); + } + } + + Err(Error::AuthenticationFailed) + } + + fn install_with_sudo( + &self, + pkg_path: &Path, + password: &str, + install_cmd: &str, + install_arg: &str, + ) -> Result { + use std::io::Write; + use std::process::{Command, Stdio}; + + let mut child = Command::new("sudo") + .arg("-S") // read password from stdin + .arg(install_cmd) + .arg(install_arg) + .arg(pkg_path) + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .spawn()?; + + if let Some(mut stdin) = child.stdin.take() { + // Write password to stdin + writeln!(stdin, "{password}")?; + } + + let status = child.wait()?; + Ok(status.success()) + } +} + +/// MacOS +#[cfg(target_os = "macos")] +impl Update { + /// ### Expected structure: + /// ├── [AppName]_[version]_x64.app.tar.gz # GZ generated by tauri-bundler + /// │ └──[AppName].app # Main application + /// │ └── Contents # Application contents... + /// │ └── ... + /// └── ... + fn install_inner(&self, bytes: &[u8]) -> Result<()> { + use flate2::read::GzDecoder; + + let cursor = Cursor::new(bytes); + let mut extracted_files: Vec = Vec::new(); + + // Create temp directories for backup and extraction + let tmp_backup_dir = tempfile::Builder::new() + .prefix("tauri_current_app") + .tempdir()?; + + let tmp_extract_dir = tempfile::Builder::new() + .prefix("tauri_updated_app") + .tempdir()?; + + let decoder = GzDecoder::new(cursor); + let mut archive = tar::Archive::new(decoder); + + // Extract files to temporary directory + for entry in archive.entries()? { + let mut entry = entry?; + let collected_path: PathBuf = entry.path()?.iter().skip(1).collect(); + let extraction_path = tmp_extract_dir.path().join(&collected_path); + + // Ensure parent directories exist + if let Some(parent) = extraction_path.parent() { + std::fs::create_dir_all(parent)?; + } + + if let Err(err) = entry.unpack(&extraction_path) { + // Cleanup on error + std::fs::remove_dir_all(tmp_extract_dir.path()).ok(); + return Err(err.into()); + } + extracted_files.push(extraction_path); + } + + // Try to move the current app to backup + let move_result = std::fs::rename( + &self.extract_path, + tmp_backup_dir.path().join("current_app"), + ); + let need_authorization = if let Err(err) = move_result { + if err.kind() == std::io::ErrorKind::PermissionDenied { + true + } else { + std::fs::remove_dir_all(tmp_extract_dir.path()).ok(); + return Err(err.into()); + } + } else { + false + }; + + if need_authorization { + log::debug!("app installation needs admin privileges"); + // Use AppleScript to perform moves with admin privileges + let apple_script = format!( + "do shell script \"rm -rf '{src}' && mv -f '{new}' '{src}'\" with administrator privileges", + src = self.extract_path.display(), + new = tmp_extract_dir.path().display() + ); + + let (tx, rx) = std::sync::mpsc::channel(); + let res = (self.context.run_on_main_thread)(Box::new(move || { + let mut script = + osakit::Script::new_from_source(osakit::Language::AppleScript, &apple_script); + script.compile().expect("invalid AppleScript"); + let r = script.execute(); + tx.send(r).unwrap(); + })); + let result = rx.recv().unwrap(); + + if res.is_err() || result.is_err() { + std::fs::remove_dir_all(tmp_extract_dir.path()).ok(); + return Err(Error::Io(std::io::Error::new( + std::io::ErrorKind::PermissionDenied, + "Failed to move the new app into place", + ))); + } + } else { + // Remove existing directory if it exists + if self.extract_path.exists() { + std::fs::remove_dir_all(&self.extract_path)?; + } + // Move the new app to the target path + std::fs::rename(tmp_extract_dir.path(), &self.extract_path)?; + } + + let _ = std::process::Command::new("touch") + .arg(&self.extract_path) + .status(); + + Ok(()) + } +} + +/// Gets the base target string used by the updater. If bundle type is available it +/// will be added to this string when selecting the download URL and signature. +/// `tauri::utils::platform::bundle_type` method is used to obtain current bundle type. +pub fn target() -> Option { + if let (Some(target), Some(arch)) = (updater_os(), updater_arch()) { + Some(format!("{target}-{arch}")) + } else { + None + } +} + +fn updater_os() -> Option<&'static str> { + if cfg!(target_os = "linux") { + Some("linux") + } else if cfg!(target_os = "macos") { + // TODO shouldn't this be macos instead? + Some("darwin") + } else if cfg!(target_os = "windows") { + Some("windows") + } else { + None + } +} + +fn updater_arch() -> Option<&'static str> { + if cfg!(target_arch = "x86") { + Some("i686") + } else if cfg!(target_arch = "x86_64") { + Some("x86_64") + } else if cfg!(target_arch = "arm") { + Some("armv7") + } else if cfg!(target_arch = "aarch64") { + Some("aarch64") + } else if cfg!(target_arch = "riscv64") { + Some("riscv64") + } else { + None + } +} + +/// Resolves the path the update must be installed to from the path of the application executable. +/// +/// This is the directory holding the executable, except on macOS where the `.app` bundle path is +/// returned for executables living in `Contents/MacOS`. +/// +/// # Errors +/// +/// Returns [`Error::FailedToDetermineExtractPath`] when the path has no parent directory. +pub fn extract_path_from_executable(executable_path: &Path) -> Result { + // Return the path of the current executable by default + // Example C:\Program Files\My App\ + let extract_path = executable_path + .parent() + .map(PathBuf::from) + .ok_or(Error::FailedToDetermineExtractPath)?; + + // MacOS example binary is in /Applications/TestApp.app/Contents/MacOS/myApp + // We need to get /Applications/.app + // TODO(lemarier): Need a better way here + // Maybe we could search for <*.app> to get the right path + #[cfg(target_os = "macos")] + if extract_path + .display() + .to_string() + .contains("Contents/MacOS") + { + return extract_path + .parent() + .map(PathBuf::from) + .ok_or(Error::FailedToDetermineExtractPath)? + .parent() + .map(PathBuf::from) + .ok_or(Error::FailedToDetermineExtractPath); + } + + Ok(extract_path) +} + +impl<'de> Deserialize<'de> for RemoteRelease { + fn deserialize(deserializer: D) -> std::result::Result + where + D: Deserializer<'de>, + { + #[derive(Deserialize)] + struct InnerRemoteRelease { + #[serde(alias = "name", deserialize_with = "parse_version")] + version: Version, + notes: Option, + pub_date: Option, + platforms: Option>, + // dynamic platform response + url: Option, + signature: Option, + } + + let release = InnerRemoteRelease::deserialize(deserializer)?; + + let pub_date = if let Some(date) = release.pub_date { + Some( + OffsetDateTime::parse(&date, &time::format_description::well_known::Rfc3339) + .map_err(|e| DeError::custom(format!("invalid value for `pub_date`: {e}")))?, + ) + } else { + None + }; + + Ok(RemoteRelease { + version: release.version, + notes: release.notes, + pub_date, + data: if let Some(platforms) = release.platforms { + RemoteReleaseInner::Static { platforms } + } else { + RemoteReleaseInner::Dynamic(ReleaseManifestPlatform { + url: release.url.ok_or_else(|| { + DeError::custom("the `url` field was not set on the updater response") + })?, + signature: release.signature.ok_or_else(|| { + DeError::custom("the `signature` field was not set on the updater response") + })?, + }) + }, + }) + } +} + +fn installer_for_bundle_type(bundle: Option) -> Option { + match bundle? { + BundleType::Deb => Some(Installer::Deb), + BundleType::Rpm => Some(Installer::Rpm), + BundleType::AppImage => Some(Installer::AppImage), + BundleType::Msi => Some(Installer::Msi), + BundleType::Nsis => Some(Installer::Nsis), + BundleType::App => Some(Installer::App), // App is also returned for Dmg type + _ => None, + } +} + +fn parse_version<'de, D>(deserializer: D) -> std::result::Result +where + D: serde::Deserializer<'de>, +{ + let str = String::deserialize(deserializer)?; + + Version::from_str(str.trim_start_matches('v')).map_err(serde::de::Error::custom) +} + +// Validate signature +fn verify_signature( + data: &[u8], + release_signature: &str, + pub_key: &str, + announced_version: &str, + require_signed_version: bool, +) -> Result<()> { + // we need to convert the pub key + let pub_key_decoded = base64_to_string(pub_key)?; + let public_key = PublicKey::decode(&pub_key_decoded)?; + let signature_base64_decoded = base64_to_string(release_signature)?; + let signature = Signature::decode(&signature_base64_decoded)?; + + // Validate signature or bail out + public_key.verify(data, &signature, true)?; + + // Only now is the trusted comment usable: minisign's global signature covers it, and + // `verify` above is what checks that global signature. Reading it before this point would + // be trusting attacker controlled data. + verify_signed_version( + signature.trusted_comment(), + announced_version, + require_signed_version, + ) +} + +/// Checks the version the artifact was signed for against the version the update endpoint +/// announced. +/// +/// The endpoint response is not signed, so its `version` field on its own does not prove which +/// release the `url` and `signature` actually point at. Comparing it against the signed version +/// is what stops a tampered response from pairing a new version number with an older release. +fn verify_signed_version( + trusted_comment: &str, + announced_version: &str, + require_signed_version: bool, +) -> Result<()> { + let Some(signed_version) = signed_version(trusted_comment) else { + // Signatures produced before the Tauri CLI started recording the version carry none, so + // this can only be enforced when the app opts in. Note that leaving it off means an + // attacker can bypass the check outright by serving one of those older signatures. + return if require_signed_version { + Err(Error::MissingSignedVersion) + } else { + Ok(()) + }; + }; + + // compare as semver so that equivalent spellings like `1.2.3` and `v1.2.3` match, falling + // back to a literal comparison for versions that are not valid semver + let matches = match ( + Version::from_str(signed_version.trim_start_matches('v')), + Version::from_str(announced_version.trim_start_matches('v')), + ) { + (Ok(signed), Ok(announced)) => signed == announced, + _ => signed_version == announced_version, + }; + + if matches { + Ok(()) + } else { + Err(Error::SignedVersionMismatch { + signed: signed_version.to_string(), + announced: announced_version.to_string(), + }) + } +} + +/// Reads the `version` field out of a signature's trusted comment, which the Tauri CLI writes as +/// tab separated `key:value` pairs, e.g. `timestamp:1700000000\tfile:app.tar.gz\tversion:1.2.3`. +fn signed_version(trusted_comment: &str) -> Option<&str> { + trusted_comment + .split('\t') + .find_map(|field| field.strip_prefix("version:")) +} + +fn base64_to_string(base64_string: &str) -> Result { + let decoded_string = &base64::engine::general_purpose::STANDARD.decode(base64_string)?; + let result = std::str::from_utf8(decoded_string) + .map_err(|_| Error::SignatureUtf8(base64_string.into()))? + .to_string(); + Ok(result) +} + +#[cfg(windows)] +fn encode_wide(string: impl AsRef) -> Vec { + use std::os::windows::ffi::OsStrExt; + + string + .as_ref() + .encode_wide() + .chain(std::iter::once(0)) + .collect() +} + +#[cfg(windows)] +trait PathExt { + fn wrap_in_quotes(&self) -> Self; +} + +#[cfg(windows)] +impl PathExt for PathBuf { + fn wrap_in_quotes(&self) -> Self { + let mut msi_path = OsString::from("\""); + msi_path.push(self.as_os_str()); + msi_path.push("\""); + PathBuf::from(msi_path) + } +} + +// adapted from https://github.com/rust-lang/rust/blob/1c047506f94cd2d05228eb992b0a6bbed1942349/library/std/src/sys/args/windows.rs#L174 +#[cfg(windows)] +fn escape_nsis_current_exe_arg(arg: impl AsRef) -> OsString { + use std::os::windows::ffi::{OsStrExt, OsStringExt}; + + let arg = arg.as_ref(); + let mut cmd: Vec = Vec::new(); + + // compared to std we additionally escape `/` so that nsis won't interpret them as a beginning of an nsis argument. + let quote = arg + .as_encoded_bytes() + .iter() + .any(|c| *c == b' ' || *c == b'\t' || *c == b'/') + || arg.is_empty(); + let escape = true; + if quote { + cmd.push('"' as u16); + } + let mut backslashes: usize = 0; + for x in arg.encode_wide() { + if escape { + if x == '\\' as u16 { + backslashes += 1; + } else { + if x == '"' as u16 { + // Add n+1 backslashes to total 2n+1 before internal '"'. + cmd.extend((0..=backslashes).map(|_| '\\' as u16)); + } + backslashes = 0; + } + } + cmd.push(x); + } + if quote { + // Add n backslashes to total 2n before ending '"'. + cmd.extend((0..backslashes).map(|_| '\\' as u16)); + cmd.push('"' as u16); + } + OsString::from_wide(&cmd) +} + +#[cfg(windows)] +fn escape_msi_property_arg(arg: impl AsRef) -> String { + let mut arg = arg.as_ref().to_string_lossy().to_string(); + + // Otherwise this argument will get lost in ShellExecute + if arg.is_empty() { + return "\"\"\"\"".to_string(); + } else if !arg.contains(' ') && !arg.contains('"') { + return arg; + } + + if arg.contains('"') { + arg = arg.replace('"', r#""""""#); + } + + if arg.starts_with('-') { + if let Some((a1, a2)) = arg.split_once('=') { + format!("{a1}=\"\"{a2}\"\"") + } else { + format!("\"\"{arg}\"\"") + } + } else { + format!("\"\"{arg}\"\"") + } +} + +#[cfg(test)] +mod tests { + use super::{signed_version, verify_signed_version}; + use crate::error::Error; + + const CURRENT: &str = "timestamp:1700000000\tfile:app_1.2.3_x64.msi.zip\tversion:1.2.3"; + // signatures produced before the CLI started embedding the version + const LEGACY: &str = "timestamp:1600000000\tfile:app_1.0.0_x64.msi.zip"; + + #[test] + fn reads_the_signed_version() { + assert_eq!(signed_version(CURRENT), Some("1.2.3")); + assert_eq!(signed_version(LEGACY), None); + // must not match on a field that merely ends in `version:` + assert_eq!(signed_version("timestamp:1\tfile:app-version:2.zip"), None); + } + + #[test] + fn accepts_a_matching_version() { + assert!(verify_signed_version(CURRENT, "1.2.3", true).is_ok()); + assert!(verify_signed_version(CURRENT, "1.2.3", false).is_ok()); + // the endpoint and the CLI may spell the same version differently + assert!(verify_signed_version(CURRENT, "v1.2.3", true).is_ok()); + } + + #[test] + fn rejects_a_version_the_artifact_was_not_signed_for() { + // the rollback the flag exists to stop: an old artifact announced as a new version + let err = verify_signed_version(CURRENT, "9.9.9", false).unwrap_err(); + assert!( + matches!(err, Error::SignedVersionMismatch { ref signed, ref announced } + if signed == "1.2.3" && announced == "9.9.9"), + "unexpected error: {err}" + ); + // rejected regardless of whether the app opted in, since the signature does say + // which version it covers + assert!(verify_signed_version(CURRENT, "9.9.9", true).is_err()); + assert!(verify_signed_version(CURRENT, "1.2.4", true).is_err()); + } + + #[test] + fn only_requires_a_signed_version_when_configured() { + assert!(verify_signed_version(LEGACY, "9.9.9", false).is_ok()); + assert!(matches!( + verify_signed_version(LEGACY, "9.9.9", true).unwrap_err(), + Error::MissingSignedVersion + )); + } + + #[test] + fn compares_non_semver_versions_literally() { + let comment = "timestamp:1700000000\tfile:app.zip\tversion:2024-01-01"; + assert!(verify_signed_version(comment, "2024-01-01", true).is_ok()); + assert!(verify_signed_version(comment, "2024-01-02", true).is_err()); + } + + #[test] + #[cfg(windows)] + fn it_wraps_correctly() { + use super::PathExt; + use std::path::PathBuf; + + assert_eq!( + PathBuf::from("C:\\Users\\Some User\\AppData\\tauri-example.exe").wrap_in_quotes(), + PathBuf::from("\"C:\\Users\\Some User\\AppData\\tauri-example.exe\"") + ) + } + + #[test] + #[cfg(windows)] + fn it_escapes_correctly_for_msi() { + use crate::updater::escape_msi_property_arg; + + // Explanation for quotes: + // The output of escape_msi_property_args() will be used in `LAUNCHAPPARGS=\"{HERE}\"`. This is the first quote level. + // To escape a quotation mark we use a second quotation mark, so "" is interpreted as " later. + // This means that the escaped strings can't ever have a single quotation mark! + // Now there are 3 major things to look out for to not break the msiexec call: + // 1) Wrap spaces in quotation marks, otherwise it will be interpreted as the end of the msiexec argument. + // 2) Escape escaping quotation marks, otherwise they will either end the msiexec argument or be ignored. + // 3) Escape emtpy args in quotation marks, otherwise the argument will get lost. + let cases = [ + "something", + "--flag", + "--empty=", + "--arg=value", + "some space", // This simulates `./my-app "some string"`. + "--arg value", // -> This simulates `./my-app "--arg value"`. Same as above but it triggers the startsWith(`-`) logic. + "--arg=unwrapped space", // `./my-app --arg="unwrapped space"` + "--arg=\"wrapped\"", // `./my-app --args=""wrapped""` + "--arg=\"wrapped space\"", // `./my-app --args=""wrapped space""` + "--arg=midword\"wrapped space\"", // `./my-app --args=midword""wrapped""` + "", // `./my-app '""'` + ]; + let cases_escaped = [ + "something", + "--flag", + "--empty=", + "--arg=value", + "\"\"some space\"\"", + "\"\"--arg value\"\"", + "--arg=\"\"unwrapped space\"\"", + r#"--arg=""""""wrapped"""""""#, + r#"--arg=""""""wrapped space"""""""#, + r#"--arg=""midword""""wrapped space"""""""#, + "\"\"\"\"", + ]; + + // Just to be sure we didn't mess that up + assert_eq!(cases.len(), cases_escaped.len()); + + for (orig, escaped) in cases.iter().zip(cases_escaped) { + assert_eq!(escape_msi_property_arg(orig), escaped); + } + } + + #[test] + #[cfg(windows)] + fn it_escapes_correctly_for_nsis() { + use crate::updater::escape_nsis_current_exe_arg; + use std::ffi::OsStr; + + let cases = [ + "something", + "--flag", + "--empty=", + "--arg=value", + "some space", // This simulates `./my-app "some string"`. + "--arg value", // -> This simulates `./my-app "--arg value"`. Same as above but it triggers the startsWith(`-`) logic. + "--arg=unwrapped space", // `./my-app --arg="unwrapped space"` + "--arg=\"wrapped\"", // `./my-app --args=""wrapped""` + "--arg=\"wrapped space\"", // `./my-app --args=""wrapped space""` + "--arg=midword\"wrapped space\"", // `./my-app --args=midword""wrapped""` + "", // `./my-app '""'` + ]; + // Note: These may not be the results we actually want (monitor this!). + // We only make sure the implementation doesn't unintentionally change. + let cases_escaped = [ + "something", + "--flag", + "--empty=", + "--arg=value", + "\"some space\"", + "\"--arg value\"", + "\"--arg=unwrapped space\"", + "--arg=\\\"wrapped\\\"", + "\"--arg=\\\"wrapped space\\\"\"", + "\"--arg=midword\\\"wrapped space\\\"\"", + "\"\"", + ]; + + // Just to be sure we didn't mess that up + assert_eq!(cases.len(), cases_escaped.len()); + + for (orig, escaped) in cases.iter().zip(cases_escaped) { + assert_eq!(escape_nsis_current_exe_arg(&OsStr::new(orig)), escaped); + } + } +} diff --git a/plugins/updater/tests/app-updater/.gitignore b/plugins/updater/tests/app-updater/.gitignore new file mode 100644 index 0000000..6a4bb53 --- /dev/null +++ b/plugins/updater/tests/app-updater/.gitignore @@ -0,0 +1 @@ +/gen/schemas diff --git a/plugins/updater/tests/app-updater/Cargo.toml b/plugins/updater/tests/app-updater/Cargo.toml new file mode 100644 index 0000000..6ab6d79 --- /dev/null +++ b/plugins/updater/tests/app-updater/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "app-updater" +version = "0.1.0" +edition = { workspace = true } + +[build-dependencies] +tauri-build = { workspace = true } + +[dependencies] +tauri = { workspace = true, features = ["wry", "common-controls-v6", "x11"] } +serde = { workspace = true } +serde_json = { workspace = true } +tauri-plugin-updater = { path = "../.." } +tiny_http = "0.12" +time = { version = "0.3", features = ["formatting"] } diff --git a/plugins/updater/tests/app-updater/build.rs b/plugins/updater/tests/app-updater/build.rs new file mode 100644 index 0000000..5ebf8d2 --- /dev/null +++ b/plugins/updater/tests/app-updater/build.rs @@ -0,0 +1,7 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +fn main() { + tauri_build::build() +} diff --git a/plugins/updater/tests/app-updater/icons/128x128.png b/plugins/updater/tests/app-updater/icons/128x128.png new file mode 100644 index 0000000000000000000000000000000000000000..77e7d2338e9d8ccffc731198dc584906627c903f GIT binary patch literal 11059 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
j#(n6`cTK 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 Command { + #[cfg(target_os = "windows")] + let cmd = { + let mut cmd = Command::new("cmd"); + cmd.arg("/c").arg("npm"); + cmd + }; + #[cfg(not(target_os = "windows"))] + let cmd = Command::new("npm"); + cmd +} + +mod v1 { + use super::{npm_command, BundleTarget, UPDATER_PRIVATE_KEY}; + use serde::Serialize; + use std::{ + path::{Path, PathBuf}, + process::Command, + }; + + #[derive(Serialize)] + pub struct PackageConfig { + pub version: &'static str, + } + + #[derive(Serialize)] + pub struct Config { + pub package: PackageConfig, + } + + pub fn build_app(cwd: &Path, config: &Config, bundle_updater: bool, target: BundleTarget) { + let mut command = npm_command(); + command + .args(["run", "tauri", "--", "build", "--debug", "--verbose"]) + .arg("--config") + .arg(serde_json::to_string(config).unwrap()) + .env("TAURI_PRIVATE_KEY", UPDATER_PRIVATE_KEY) + .env("TAURI_KEY_PASSWORD", "") + .current_dir(cwd); + + #[cfg(target_os = "linux")] + command.args(["--bundles", target.name()]); + #[cfg(target_os = "macos")] + command.args(["--bundles", target.name()]); + + if bundle_updater { + #[cfg(windows)] + command.args(["--bundles", "msi", "nsis"]); + } else { + #[cfg(windows)] + command.args(["--bundles", target.name()]); + } + + let status = command + .status() + .expect("failed to run Tauri CLI to bundle v1 app"); + + if !status.success() { + panic!("failed to bundle v1 app {:?}", status.code()); + } + } + + #[cfg(target_os = "linux")] + pub fn bundle_paths(root_dir: &Path, version: &str) -> Vec<(BundleTarget, PathBuf)> { + vec![( + BundleTarget::AppImage, + root_dir.join(format!( + "target/debug/bundle/appimage/app-updater-v1_{version}_amd64.AppImage", + )), + )] + } + + #[cfg(target_os = "macos")] + pub fn bundle_paths(root_dir: &Path, _version: &str) -> Vec<(BundleTarget, PathBuf)> { + vec![( + BundleTarget::App, + root_dir.join("target/debug/bundle/macos/app-updater-v1.app"), + )] + } + + #[cfg(target_os = "ios")] + pub fn bundle_paths(root_dir: &Path, _version: &str) -> Vec<(BundleTarget, PathBuf)> { + vec![( + BundleTarget::App, + root_dir.join("target/debug/bundle/ios/app-updater-v1.ipa"), + )] + } + + #[cfg(target_os = "android")] + pub fn bundle_path(root_dir: &Path, _version: &str) -> PathBuf { + root_dir.join("target/debug/bundle/android/app-updater-v1.apk") + } + + #[cfg(windows)] + pub fn bundle_paths(root_dir: &Path, version: &str) -> Vec<(BundleTarget, PathBuf)> { + vec![ + ( + BundleTarget::Nsis, + root_dir.join(format!( + "target/debug/bundle/nsis/app-updater-v1_{version}_x64-setup.exe" + )), + ), + ( + BundleTarget::Msi, + root_dir.join(format!( + "target/debug/bundle/msi/app-updater-v1_{version}_x64_en-US.msi" + )), + ), + ] + } +} + +mod v2 { + + use super::{BundleTarget, UPDATER_PRIVATE_KEY}; + use serde::Serialize; + use std::{ + path::{Path, PathBuf}, + process::Command, + }; + use tauri::utils::config::Updater; + + #[derive(Serialize)] + pub struct Config { + pub version: &'static str, + pub bundle: BundleConfig, + } + + #[derive(Serialize)] + #[serde(rename_all = "camelCase")] + pub struct BundleConfig { + pub create_updater_artifacts: Updater, + } + + pub fn build_app(cwd: &Path, config: &Config, bundle_updater: bool, target: BundleTarget) { + let mut command = Command::new("cargo"); + command + .args(["tauri", "build", "--debug", "--verbose"]) + .arg("--config") + .arg(serde_json::to_string(config).unwrap()) + .env("TAURI_SIGNING_PRIVATE_KEY", UPDATER_PRIVATE_KEY) + .env("TAURI_SIGNING_PRIVATE_KEY_PASSWORD", "") + .current_dir(cwd); + + #[cfg(target_os = "linux")] + command.args(["--bundles", target.name()]); + #[cfg(target_os = "macos")] + command.args(["--bundles", target.name()]); + + if bundle_updater { + #[cfg(windows)] + command.args(["--bundles", "msi", "nsis"]); + } else { + #[cfg(windows)] + command.args(["--bundles", target.name()]); + } + + let status = command + .status() + .expect("failed to run Tauri CLI to bundle v2 app"); + + if !status.success() { + panic!("failed to bundle v2 app {:?}", status.code()); + } + } + + #[cfg(target_os = "linux")] + pub fn bundle_paths(root_dir: &Path, version: &str) -> Vec<(BundleTarget, PathBuf)> { + vec![( + BundleTarget::AppImage, + root_dir.join(format!( + "target/debug/bundle/appimage/app-updater-v2_{version}_amd64.AppImage", + )), + )] + } + + #[cfg(target_os = "macos")] + pub fn bundle_paths(root_dir: &Path, _version: &str) -> Vec<(BundleTarget, PathBuf)> { + vec![( + BundleTarget::App, + root_dir.join("target/debug/bundle/macos/app-updater-v2.app"), + )] + } + + #[cfg(target_os = "ios")] + pub fn bundle_paths(root_dir: &Path, _version: &str) -> Vec<(BundleTarget, PathBuf)> { + vec![( + BundleTarget::App, + root_dir.join("target/debug/bundle/ios/app-updater-v2.ipa"), + )] + } + + #[cfg(target_os = "android")] + pub fn bundle_path(root_dir: &Path, _version: &str) -> PathBuf { + root_dir.join("target/debug/bundle/android/app-updater-v2.apk") + } + + #[cfg(windows)] + pub fn bundle_paths(root_dir: &Path, version: &str) -> Vec<(BundleTarget, PathBuf)> { + vec![ + ( + BundleTarget::Nsis, + root_dir.join(format!( + "target/debug/bundle/nsis/app-updater-v2_{version}_x64-setup.exe" + )), + ), + ( + BundleTarget::Msi, + root_dir.join(format!( + "target/debug/bundle/msi/app-updater-v2_{version}_x64_en-US.msi" + )), + ), + ] + } +} + +#[derive(Serialize)] +struct PlatformUpdate { + signature: String, + url: &'static str, + with_elevated_task: bool, +} + +#[derive(Serialize)] +struct Update { + version: &'static str, + date: String, + platforms: HashMap, +} + +#[derive(Copy, Clone)] +enum BundleTarget { + AppImage, + + App, + + Msi, + Nsis, +} + +impl BundleTarget { + fn name(self) -> &'static str { + match self { + Self::AppImage => "appimage", + Self::App => "app", + Self::Msi => "msi", + Self::Nsis => "nsis", + } + } +} + +impl Default for BundleTarget { + fn default() -> Self { + #[cfg(any(target_os = "macos", target_os = "ios"))] + return Self::App; + #[cfg(target_os = "linux")] + return Self::AppImage; + #[cfg(windows)] + return Self::Nsis; + } +} + +#[test] +#[ignore] +fn update_app() { + let target = + tauri_plugin_updater::target().expect("running updater test in an unsupported platform"); + let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")); + let root_dir = manifest_dir.join("../../../.."); + let v1_root_dir = manifest_dir.join("v1-app"); + let v2_root_dir = manifest_dir.join("v2-app"); + + let status = npm_command() + .arg("install") + .current_dir(&v1_root_dir) + .status() + .expect("failed to run npm install"); + if !status.success() { + panic!("failed to run npm install"); + } + + let v2_config = v2::Config { + version: "1.0.0", + bundle: v2::BundleConfig { + create_updater_artifacts: Updater::String(V1Compatible::V1Compatible), + }, + }; + + // bundle app update (v2) + v2::build_app(&v2_root_dir, &v2_config, true, Default::default()); + + let updater_zip_ext = if cfg!(windows) { "zip" } else { "tar.gz" }; + + for (bundle_target, out_bundle_path) in v2::bundle_paths(&root_dir, "1.0.0") { + let bundle_updater_ext = out_bundle_path + .extension() + .unwrap() + .to_str() + .unwrap() + .replace("exe", "nsis"); + let updater_extension = format!("{bundle_updater_ext}.{updater_zip_ext}"); + let signature_extension = format!("{updater_extension}.sig"); + let signature_path = out_bundle_path.with_extension(signature_extension); + let signature = std::fs::read_to_string(&signature_path).unwrap_or_else(|_| { + panic!("failed to read signature file {}", signature_path.display()) + }); + let out_updater_path = out_bundle_path.with_extension(updater_extension); + let updater_path = root_dir.join(format!( + "target/debug/{}", + out_updater_path.file_name().unwrap().to_str().unwrap() + )); + std::fs::rename(&out_updater_path, &updater_path).expect("failed to rename bundle"); + + let target = target.clone(); + + // start the updater server + let server = Arc::new( + tiny_http::Server::http("localhost:3007").expect("failed to start updater server"), + ); + + let server_ = server.clone(); + std::thread::spawn(move || { + for request in server_.incoming_requests() { + match request.url() { + "/" => { + let mut platforms = HashMap::new(); + + platforms.insert( + target.clone(), + PlatformUpdate { + signature: signature.clone(), + url: "http://localhost:3007/download", + with_elevated_task: false, + }, + ); + let body = serde_json::to_vec(&Update { + version: "1.0.0", + date: time::OffsetDateTime::now_utc() + .format(&time::format_description::well_known::Rfc3339) + .unwrap(), + platforms, + }) + .unwrap(); + let len = body.len(); + let response = tiny_http::Response::new( + tiny_http::StatusCode(200), + Vec::new(), + std::io::Cursor::new(body), + Some(len), + None, + ); + let _ = request.respond(response); + } + "/download" => { + let _ = request.respond(tiny_http::Response::from_file( + File::open(&updater_path).unwrap_or_else(|_| { + panic!("failed to open updater bundle {}", updater_path.display()) + }), + )); + } + _ => (), + } + } + }); + + let v1_config = v1::Config { + package: v1::PackageConfig { version: "0.1.0" }, + }; + + // bundle initial app version (tauri v1) + v1::build_app(&v1_root_dir, &v1_config, false, bundle_target); + + let status_checks = if matches!(bundle_target, BundleTarget::Msi) { + // for msi we can't really check if the app was updated, because we can't change the install path + vec![(UPDATED_EXIT_CODE, 1)] + } else { + vec![(UPDATED_EXIT_CODE, 1), (UP_TO_DATE_EXIT_CODE, 2)] + }; + + for (expected_exit_code, expected_tauri_version) in status_checks { + let (expected_app_version, bundle_paths_fn, app_name_suffix) = + match expected_tauri_version { + 1 => ( + v1_config.package.version, + Box::new(|| v1::bundle_paths(&v1_root_dir, v1_config.package.version)) + as Box Vec<(BundleTarget, PathBuf)>>, + "-v1", + ), + 2 => ( + v2_config.version, + Box::new(|| v2::bundle_paths(&root_dir, v2_config.version)) + as Box Vec<(BundleTarget, PathBuf)>>, + "-v2", + ), + _ => panic!("unknown tauri version"), + }; + let mut binary_cmd = if cfg!(windows) { + let app_root_dir = match expected_tauri_version { + 1 => &v1_root_dir, + 2 => &root_dir, + _ => panic!("unknown tauri version"), + }; + Command::new( + app_root_dir.join(format!("target/debug/app-updater{app_name_suffix}.exe")), + ) + } else if cfg!(target_os = "macos") { + Command::new( + bundle_paths_fn() + .first() + .unwrap() + .1 + .join(format!("Contents/MacOS/app-updater{app_name_suffix}")), + ) + } else if std::env::var("CI").map(|v| v == "true").unwrap_or_default() { + let mut c = Command::new("xvfb-run"); + c.arg("--auto-servernum") + .arg(&bundle_paths_fn().first().unwrap().1); + c + } else { + Command::new(&bundle_paths_fn().first().unwrap().1) + }; + + binary_cmd.env("TARGET", bundle_target.name()); + + let output = binary_cmd.output().expect("failed to run app"); + let stdout = String::from_utf8_lossy(&output.stdout); + + println!("{stdout}"); + eprintln!("{}", String::from_utf8_lossy(&output.stderr)); + + let code = output.status.code().unwrap_or(-1); + + if code != expected_exit_code { + panic!("failed to run app, expected exit code {expected_exit_code}, got {code}"); + } + if !stdout.contains(&format!("version={expected_app_version}")) { + panic!("app version does not match {expected_app_version}"); + } + #[cfg(windows)] + if code == UPDATED_EXIT_CODE { + // wait for the update to finish + std::thread::sleep(std::time::Duration::from_secs(5)); + } + } + + server.unblock(); + } +} diff --git a/plugins/updater/tests/updater-migration/v1-app/.gitignore b/plugins/updater/tests/updater-migration/v1-app/.gitignore new file mode 100644 index 0000000..c6bfc74 --- /dev/null +++ b/plugins/updater/tests/updater-migration/v1-app/.gitignore @@ -0,0 +1,3 @@ +target/ +package-lock.json +node_modules/ \ No newline at end of file diff --git a/plugins/updater/tests/updater-migration/v1-app/Cargo.lock b/plugins/updater/tests/updater-migration/v1-app/Cargo.lock new file mode 100644 index 0000000..54604dd --- /dev/null +++ b/plugins/updater/tests/updater-migration/v1-app/Cargo.lock @@ -0,0 +1,4361 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "addr2line" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e4503c46a5c0c7844e948c9a4d6acd9f50cccb4de1c48eb9e291ea17470c678" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" + +[[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", +] + +[[package]] +name = "alloc-no-stdlib" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3" + +[[package]] +name = "alloc-stdlib" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94fb8275041c72129eb51b7d0322c29b8387a0386127718b096429201a5d6ece" +dependencies = [ + "alloc-no-stdlib", +] + +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "anyhow" +version = "1.0.86" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da" + +[[package]] +name = "app-updater-v1" +version = "0.1.0" +dependencies = [ + "serde", + "serde_json", + "tauri", + "tauri-build", + "tiny_http", +] + +[[package]] +name = "ascii" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d92bec98840b8f03a5ff5413de5293bfcd8bf96467cf5452609f939ec6f5de16" + +[[package]] +name = "atk" +version = "0.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c3d816ce6f0e2909a96830d6911c2aff044370b1ef92d7f267b43bae5addedd" +dependencies = [ + "atk-sys", + "bitflags 1.3.2", + "glib", + "libc", +] + +[[package]] +name = "atk-sys" +version = "0.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58aeb089fb698e06db8089971c7ee317ab9644bade33383f63631437b03aafb6" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps 6.2.2", +] + +[[package]] +name = "autocfg" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" + +[[package]] +name = "backtrace" +version = "0.3.73" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cc23269a4f8976d0a4d2e7109211a419fe30e8d88d677cd60b6bc79c5732e0a" +dependencies = [ + "addr2line", + "cc", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", +] + +[[package]] +name = "base64" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" + +[[package]] +name = "base64" +version = "0.21.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" + +[[package]] +name = "block" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a" + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "brotli" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d640d25bc63c50fb1f0b545ffd80207d2e10a4c965530809b40ba3386825c391" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", + "brotli-decompressor", +] + +[[package]] +name = "brotli-decompressor" +version = "2.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e2e4afe60d7dd600fdd3de8d0f08c2b7ec039712e3b6137ff98b7004e82de4f" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", +] + +[[package]] +name = "bs58" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf88ba1141d185c399bee5288d850d63b8369520c1eafc32a0430b5b6c287bf4" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "bstr" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40723b8fb387abc38f4f4a37c09073622e41dd12327033091ef8950659e6dc0c" +dependencies = [ + "memchr", + "serde", +] + +[[package]] +name = "bumpalo" +version = "3.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" + +[[package]] +name = "bytemuck" +version = "1.16.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "102087e286b4677862ea56cf8fc58bb2cdfa8725c40ffb80fe3a008eb7f2fc83" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" +dependencies = [ + "serde", +] + +[[package]] +name = "cairo-rs" +version = "0.15.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c76ee391b03d35510d9fa917357c7f1855bd9a6659c95a1b392e33f49b3369bc" +dependencies = [ + "bitflags 1.3.2", + "cairo-sys-rs", + "glib", + "libc", + "thiserror", +] + +[[package]] +name = "cairo-sys-rs" +version = "0.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c55d429bef56ac9172d25fecb85dc8068307d17acd74b377866b7a1ef25d3c8" +dependencies = [ + "glib-sys", + "libc", + "system-deps 6.2.2", +] + +[[package]] +name = "cargo_toml" +version = "0.15.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "599aa35200ffff8f04c1925aa1acc92fa2e08874379ef42e210a80e527e60838" +dependencies = [ + "serde", + "toml 0.7.8", +] + +[[package]] +name = "cc" +version = "1.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "504bdec147f2cc13c8b57ed9401fd8a147cc66b67ad5cb241394244f2c947549" + +[[package]] +name = "cesu8" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" + +[[package]] +name = "cfb" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d38f2da7a0a2c4ccf0065be06397cc26a81f4e528be095826eee9d4adbb8c60f" +dependencies = [ + "byteorder", + "fnv", + "uuid", +] + +[[package]] +name = "cfg-expr" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3431df59f28accaf4cb4eed4a9acc66bea3f3c3753aa6cdc2f024174ef232af7" +dependencies = [ + "smallvec", +] + +[[package]] +name = "cfg-expr" +version = "0.15.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d067ad48b8650848b989a59a86c6c36a995d02d2bf778d45c3c5d57bc2718f02" +dependencies = [ + "smallvec", + "target-lexicon", +] + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "chrono" +version = "0.4.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "num-traits", + "serde", + "windows-targets 0.52.6", +] + +[[package]] +name = "chunked_transfer" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e4de3bc4ea267985becf712dc6d9eed8b04c953b3fcfb339ebc87acd9804901" + +[[package]] +name = "cocoa" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f425db7937052c684daec3bd6375c8abe2d146dca4b8b143d6db777c39138f3a" +dependencies = [ + "bitflags 1.3.2", + "block", + "cocoa-foundation", + "core-foundation", + "core-graphics", + "foreign-types", + "libc", + "objc", +] + +[[package]] +name = "cocoa-foundation" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c6234cbb2e4c785b456c0644748b1ac416dd045799740356f8363dfe00c93f7" +dependencies = [ + "bitflags 1.3.2", + "block", + "core-foundation", + "core-graphics-types", + "libc", + "objc", +] + +[[package]] +name = "color_quant" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" + +[[package]] +name = "combine" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" +dependencies = [ + "bytes", + "memchr", +] + +[[package]] +name = "convert_case" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" + +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" + +[[package]] +name = "core-graphics" +version = "0.22.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2581bbab3b8ffc6fcbd550bf46c355135d16e9ff2a6ea032ad6b9bf1d7efe4fb" +dependencies = [ + "bitflags 1.3.2", + "core-foundation", + "core-graphics-types", + "foreign-types", + "libc", +] + +[[package]] +name = "core-graphics-types" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45390e6114f68f718cc7a830514a96f903cccd70d02a8f6d9f643ac4ba45afaf" +dependencies = [ + "bitflags 1.3.2", + "core-foundation", + "libc", +] + +[[package]] +name = "cpufeatures" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53fe5e26ff1b7aef8bca9c6080520cfb8d9333c7568e1829cef191a9723e5504" +dependencies = [ + "libc", +] + +[[package]] +name = "crc32fast" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam-channel" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613f8cc01fe9cf1a3eb3d7f488fd2fa8388403e97039e2f73692932e291a770d" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80" + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "cssparser" +version = "0.27.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "754b69d351cdc2d8ee09ae203db831e005560fc6030da058f86ad60c92a9cb0a" +dependencies = [ + "cssparser-macros", + "dtoa-short", + "itoa 0.4.8", + "matches", + "phf 0.8.0", + "proc-macro2", + "quote", + "smallvec", + "syn 1.0.109", +] + +[[package]] +name = "cssparser-macros" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13b588ba4ac1a99f7f2964d24b3d896ddc6bf847ee3855dbd4366f058cfcd331" +dependencies = [ + "quote", + "syn 2.0.87", +] + +[[package]] +name = "ctor" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edb49164822f3ee45b17acd4a208cfc1251410cf0cad9a833234c9890774dd9f" +dependencies = [ + "quote", + "syn 2.0.87", +] + +[[package]] +name = "darling" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9865a50f7c335f53564bb694ef660825eb8610e0a53d3e11bf1b0d3df31e03b0" +dependencies = [ + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 2.0.87", +] + +[[package]] +name = "darling_macro" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" +dependencies = [ + "darling_core", + "quote", + "syn 2.0.87", +] + +[[package]] +name = "deranged" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ececcb659e7ba858fb4f10388c250a7252eb0a27373f1a72b8748afdd248e587" +dependencies = [ + "powerfmt", + "serde_core", +] + +[[package]] +name = "derive_more" +version = "0.99.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f33878137e4dafd7fa914ad4e259e18a4e8e532b9617a2d0150262bf53abfce" +dependencies = [ + "convert_case", + "proc-macro2", + "quote", + "rustc_version", + "syn 2.0.87", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + +[[package]] +name = "dirs-next" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b98cf8ebf19c3d1b223e151f99a4f9f0690dca41414773390fc824184ac833e1" +dependencies = [ + "cfg-if", + "dirs-sys-next", +] + +[[package]] +name = "dirs-sys-next" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d" +dependencies = [ + "libc", + "redox_users", + "winapi", +] + +[[package]] +name = "dispatch" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd0c93bb4b0c6d9b77f4435b0ae98c24d17f1c45b2ff844c6151a07256ca923b" + +[[package]] +name = "dtoa" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcbb2bf8e87535c23f7a8a321e364ce21462d0ff10cb6407820e8e96dfff6653" + +[[package]] +name = "dtoa-short" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd1511a7b6a56299bd043a9c167a6d2bfb37bf84a6dfceaba651168adfb43c87" +dependencies = [ + "dtoa", +] + +[[package]] +name = "dunce" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" + +[[package]] +name = "dyn-clone" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" + +[[package]] +name = "embed-resource" +version = "2.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4edcacde9351c33139a41e3c97eb2334351a81a2791bebb0b243df837128f602" +dependencies = [ + "cc", + "memchr", + "rustc_version", + "toml 0.8.19", + "vswhom", + "winreg 0.52.0", +] + +[[package]] +name = "embed_plist" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ef6b89e5b37196644d8796de5268852ff179b44e96276cf4290264843743bb7" + +[[package]] +name = "encoding_rs" +version = "0.8.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b45de904aa0b010bce2ab45264d0631681847fa7b6f2eaa7dab7619943bc4f59" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "equivalent" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" + +[[package]] +name = "errno" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "fastrand" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fc0510504f03c51ada170672ac806f1f105a88aa97a5281117e1ddc3368e51a" + +[[package]] +name = "fdeflate" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f9bfee30e4dedf0ab8b422f03af778d9612b63f502710fc500a334ebe2de645" +dependencies = [ + "simd-adler32", +] + +[[package]] +name = "field-offset" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38e2275cc4e4fc009b0669731a1e5ab7ebf11f469eaede2bab9309a5b4d6057f" +dependencies = [ + "memoffset", + "rustc_version", +] + +[[package]] +name = "filetime" +version = "0.2.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ee447700ac8aa0b2f2bd7bc4462ad686ba06baa6727ac149a2d6277f0d240fd" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall 0.4.1", + "windows-sys 0.52.0", +] + +[[package]] +name = "flate2" +version = "1.0.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f211bbe8e69bbd0cfdea405084f128ae8b4aaa6b0b522fc8f2b009084797920" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + +[[package]] +name = "form_urlencoded" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futf" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df420e2e84819663797d1ec6544b13c5be84629e7bb00dc960d6917db2987843" +dependencies = [ + "mac", + "new_debug_unreachable", +] + +[[package]] +name = "futures-channel" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78" +dependencies = [ + "futures-core", +] + +[[package]] +name = "futures-core" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" + +[[package]] +name = "futures-executor" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a576fc72ae164fca6b9db127eaa9a9dda0d61316034f33a0a0d4eda41f02b01d" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1" + +[[package]] +name = "futures-macro" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.87", +] + +[[package]] +name = "futures-sink" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5" + +[[package]] +name = "futures-task" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004" + +[[package]] +name = "futures-util" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" +dependencies = [ + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", +] + +[[package]] +name = "fxhash" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c" +dependencies = [ + "byteorder", +] + +[[package]] +name = "gdk" +version = "0.15.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6e05c1f572ab0e1f15be94217f0dc29088c248b14f792a5ff0af0d84bcda9e8" +dependencies = [ + "bitflags 1.3.2", + "cairo-rs", + "gdk-pixbuf", + "gdk-sys", + "gio", + "glib", + "libc", + "pango", +] + +[[package]] +name = "gdk-pixbuf" +version = "0.15.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad38dd9cc8b099cceecdf41375bb6d481b1b5a7cd5cd603e10a69a9383f8619a" +dependencies = [ + "bitflags 1.3.2", + "gdk-pixbuf-sys", + "gio", + "glib", + "libc", +] + +[[package]] +name = "gdk-pixbuf-sys" +version = "0.15.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "140b2f5378256527150350a8346dbdb08fadc13453a7a2d73aecd5fab3c402a7" +dependencies = [ + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "system-deps 6.2.2", +] + +[[package]] +name = "gdk-sys" +version = "0.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32e7a08c1e8f06f4177fb7e51a777b8c1689f743a7bc11ea91d44d2226073a88" +dependencies = [ + "cairo-sys-rs", + "gdk-pixbuf-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "pango-sys", + "pkg-config", + "system-deps 6.2.2", +] + +[[package]] +name = "gdkwayland-sys" +version = "0.15.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cca49a59ad8cfdf36ef7330fe7bdfbe1d34323220cc16a0de2679ee773aee2c2" +dependencies = [ + "gdk-sys", + "glib-sys", + "gobject-sys", + "libc", + "pkg-config", + "system-deps 6.2.2", +] + +[[package]] +name = "gdkx11-sys" +version = "0.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4b7f8c7a84b407aa9b143877e267e848ff34106578b64d1e0a24bf550716178" +dependencies = [ + "gdk-sys", + "glib-sys", + "libc", + "system-deps 6.2.2", + "x11", +] + +[[package]] +name = "generator" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cc16584ff22b460a382b7feec54b23d2908d858152e5739a120b949293bd74e" +dependencies = [ + "cc", + "libc", + "log", + "rustversion", + "windows 0.48.0", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce" +dependencies = [ + "cfg-if", + "libc", + "wasi 0.9.0+wasi-snapshot-preview1", +] + +[[package]] +name = "getrandom" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" +dependencies = [ + "cfg-if", + "libc", + "wasi 0.11.0+wasi-snapshot-preview1", +] + +[[package]] +name = "gimli" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40ecd4077b5ae9fd2e9e169b102c6c330d0605168eb0e8bf79952b256dbefffd" + +[[package]] +name = "gio" +version = "0.15.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68fdbc90312d462781a395f7a16d96a2b379bb6ef8cd6310a2df272771c4283b" +dependencies = [ + "bitflags 1.3.2", + "futures-channel", + "futures-core", + "futures-io", + "gio-sys", + "glib", + "libc", + "once_cell", + "thiserror", +] + +[[package]] +name = "gio-sys" +version = "0.15.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32157a475271e2c4a023382e9cab31c4584ee30a97da41d3c4e9fdd605abcf8d" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps 6.2.2", + "winapi", +] + +[[package]] +name = "glib" +version = "0.15.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edb0306fbad0ab5428b0ca674a23893db909a98582969c9b537be4ced78c505d" +dependencies = [ + "bitflags 1.3.2", + "futures-channel", + "futures-core", + "futures-executor", + "futures-task", + "glib-macros", + "glib-sys", + "gobject-sys", + "libc", + "once_cell", + "smallvec", + "thiserror", +] + +[[package]] +name = "glib-macros" +version = "0.15.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10c6ae9f6fa26f4fb2ac16b528d138d971ead56141de489f8111e259b9df3c4a" +dependencies = [ + "anyhow", + "heck 0.4.1", + "proc-macro-crate", + "proc-macro-error", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "glib-sys" +version = "0.15.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef4b192f8e65e9cf76cbf4ea71fa8e3be4a0e18ffe3d68b8da6836974cc5bad4" +dependencies = [ + "libc", + "system-deps 6.2.2", +] + +[[package]] +name = "glob" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" + +[[package]] +name = "globset" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57da3b9b5b85bd66f31093f8c408b90a74431672542466497dcbdfdc02034be1" +dependencies = [ + "aho-corasick", + "bstr", + "log", + "regex-automata 0.4.7", + "regex-syntax 0.8.4", +] + +[[package]] +name = "gobject-sys" +version = "0.15.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d57ce44246becd17153bd035ab4d32cfee096a657fc01f2231c9278378d1e0a" +dependencies = [ + "glib-sys", + "libc", + "system-deps 6.2.2", +] + +[[package]] +name = "gtk" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92e3004a2d5d6d8b5057d2b57b3712c9529b62e82c77f25c1fecde1fd5c23bd0" +dependencies = [ + "atk", + "bitflags 1.3.2", + "cairo-rs", + "field-offset", + "futures-channel", + "gdk", + "gdk-pixbuf", + "gio", + "glib", + "gtk-sys", + "gtk3-macros", + "libc", + "once_cell", + "pango", + "pkg-config", +] + +[[package]] +name = "gtk-sys" +version = "0.15.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5bc2f0587cba247f60246a0ca11fe25fb733eabc3de12d1965fc07efab87c84" +dependencies = [ + "atk-sys", + "cairo-sys-rs", + "gdk-pixbuf-sys", + "gdk-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "pango-sys", + "system-deps 6.2.2", +] + +[[package]] +name = "gtk3-macros" +version = "0.15.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "684c0456c086e8e7e9af73ec5b84e35938df394712054550e81558d21c44ab0d" +dependencies = [ + "anyhow", + "proc-macro-crate", + "proc-macro-error", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "h2" +version = "0.3.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81fe527a889e1532da5c525686d96d4c2e74cdd345badf8dfef9f6b39dd5f5e8" +dependencies = [ + "bytes", + "fnv", + "futures-core", + "futures-sink", + "futures-util", + "http", + "indexmap 2.3.0", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" + +[[package]] +name = "heck" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d621efb26863f0e9924c6ac577e8275e5e6b77455db64ffa6c65c904e9e132c" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hermit-abi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "html5ever" +version = "0.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bea68cab48b8459f17cf1c944c67ddc572d272d9f2b274140f223ecb1da4a3b7" +dependencies = [ + "log", + "mac", + "markup5ever", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "http" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1" +dependencies = [ + "bytes", + "fnv", + "itoa 1.0.11", +] + +[[package]] +name = "http-body" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2" +dependencies = [ + "bytes", + "http", + "pin-project-lite", +] + +[[package]] +name = "http-range" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21dec9db110f5f872ed9699c3ecf50cf16f423502706ba5c72462e28d3157573" + +[[package]] +name = "httparse" +version = "1.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fcc0b4a115bf80b728eb8ea024ad5bd707b615bfed49e0665b6e0f86fd082d9" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "hyper" +version = "0.14.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a152ddd61dfaec7273fe8419ab357f33aee0d914c5f4efbf0d96fa749eea5ec9" +dependencies = [ + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "httparse", + "httpdate", + "itoa 1.0.11", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", + "want", +] + +[[package]] +name = "hyper-tls" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905" +dependencies = [ + "bytes", + "hyper", + "native-tls", + "tokio", + "tokio-native-tls", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.60" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7ffbb5a1b541ea2561f8c41c087286cc091e21e556a4f09a8f6cbf17b69b141" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "ico" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3804960be0bb5e4edb1e1ad67afd321a9ecfd875c3e65c099468fd2717d7cae" +dependencies = [ + "byteorder", + "png", +] + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "idna" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6" +dependencies = [ + "unicode-bidi", + "unicode-normalization", +] + +[[package]] +name = "ignore" +version = "0.4.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b46810df39e66e925525d6e38ce1e7f6e1d208f72dc39757880fcb66e2c58af1" +dependencies = [ + "crossbeam-deque", + "globset", + "log", + "memchr", + "regex-automata 0.4.7", + "same-file", + "walkdir", + "winapi-util", +] + +[[package]] +name = "image" +version = "0.24.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5690139d2f55868e080017335e4b94cb7414274c74f1669c84fb5feba2c9f69d" +dependencies = [ + "bytemuck", + "byteorder", + "color_quant", + "num-traits", +] + +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", + "serde", +] + +[[package]] +name = "indexmap" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de3fc2e30ba82dd1b3911c8de1ffc143c74a914a14e99514d7637e3099df5ea0" +dependencies = [ + "equivalent", + "hashbrown 0.14.5", + "serde", +] + +[[package]] +name = "infer" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f551f8c3a39f68f986517db0d1759de85881894fdc7db798bd2a9df9cb04b7fc" +dependencies = [ + "cfb", +] + +[[package]] +name = "instant" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "ipnet" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3" + +[[package]] +name = "itoa" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b71991ff56294aa922b450139ee08b3bfc70982c6b2c7562771375cf73542dd4" + +[[package]] +name = "itoa" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" + +[[package]] +name = "javascriptcore-rs" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf053e7843f2812ff03ef5afe34bb9c06ffee120385caad4f6b9967fcd37d41c" +dependencies = [ + "bitflags 1.3.2", + "glib", + "javascriptcore-rs-sys", +] + +[[package]] +name = "javascriptcore-rs-sys" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "905fbb87419c5cde6e3269537e4ea7d46431f3008c5d057e915ef3f115e7793c" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps 5.0.0", +] + +[[package]] +name = "jiff" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3590fea8e9e22d449600c9bbd481a8163bef223e4ff938e5f55899f8cf1adb93" +dependencies = [ + "jiff-tzdb-platform", + "log", + "portable-atomic", + "portable-atomic-util", + "serde", + "windows-sys 0.52.0", +] + +[[package]] +name = "jiff-tzdb" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "142bd39932ad231f10513df9ab62661fead8719872150b7ad02a2df79f4e141e" + +[[package]] +name = "jiff-tzdb-platform" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "875a5a69ac2bab1a891711cf5eccbec1ce0341ea805560dcd90b7a2e925132e8" +dependencies = [ + "jiff-tzdb", +] + +[[package]] +name = "jni" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "039022cdf4d7b1cf548d31f60ae783138e5fd42013f6271049d7df7afadef96c" +dependencies = [ + "cesu8", + "combine", + "jni-sys", + "log", + "thiserror", + "walkdir", +] + +[[package]] +name = "jni-sys" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" + +[[package]] +name = "js-sys" +version = "0.3.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29c15563dc2726973df627357ce0c9ddddbea194836909d655df6a75d2cf296d" +dependencies = [ + "wasm-bindgen", +] + +[[package]] +name = "json-patch" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec9ad60d674508f3ca8f380a928cfe7b096bc729c4e2dbfe3852bc45da3ab30b" +dependencies = [ + "serde", + "serde_json", + "thiserror", +] + +[[package]] +name = "kuchikiki" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f29e4755b7b995046f510a7520c42b2fed58b77bd94d5a87a8eb43d2fd126da8" +dependencies = [ + "cssparser", + "html5ever", + "indexmap 1.9.3", + "matches", + "selectors", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "libc" +version = "0.2.155" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c" + +[[package]] +name = "libredox" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" +dependencies = [ + "bitflags 2.6.0", + "libc", +] + +[[package]] +name = "linux-raw-sys" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" + +[[package]] +name = "lock_api" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" + +[[package]] +name = "loom" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff50ecb28bb86013e935fb6683ab1f6d3a20016f123c76fd4c27470076ac30f5" +dependencies = [ + "cfg-if", + "generator", + "scoped-tls", + "serde", + "serde_json", + "tracing", + "tracing-subscriber", +] + +[[package]] +name = "mac" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4" + +[[package]] +name = "malloc_buf" +version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62bb907fe88d54d8d9ce32a3cceab4218ed2f6b7d35617cafe9adf84e43919cb" +dependencies = [ + "libc", +] + +[[package]] +name = "markup5ever" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2629bb1404f3d34c2e921f21fd34ba00b206124c81f65c50b43b6aaefeb016" +dependencies = [ + "log", + "phf 0.10.1", + "phf_codegen 0.10.0", + "string_cache", + "string_cache_codegen", + "tendril", +] + +[[package]] +name = "matchers" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558" +dependencies = [ + "regex-automata 0.1.10", +] + +[[package]] +name = "matches" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2532096657941c2fea9c289d370a250971c689d4f143798ff67113ec042024a5" + +[[package]] +name = "memchr" +version = "2.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" + +[[package]] +name = "memoffset" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" +dependencies = [ + "autocfg", +] + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "minisign-verify" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "933dca44d65cdd53b355d0b73d380a2ff5da71f87f036053188bf1eab6a19881" + +[[package]] +name = "miniz_oxide" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8a240ddb74feaf34a79a7add65a741f3167852fba007066dcac1ca548d89c08" +dependencies = [ + "adler", + "simd-adler32", +] + +[[package]] +name = "mio" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4569e456d394deccd22ce1c1913e6ea0e54519f577285001215d33557431afe4" +dependencies = [ + "hermit-abi", + "libc", + "wasi 0.11.0+wasi-snapshot-preview1", + "windows-sys 0.52.0", +] + +[[package]] +name = "native-tls" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8614eb2c83d59d1c8cc974dd3f920198647674a0a035e1af1fa58707e317466" +dependencies = [ + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", +] + +[[package]] +name = "ndk" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2032c77e030ddee34a6787a64166008da93f6a352b629261d0fee232b8742dd4" +dependencies = [ + "bitflags 1.3.2", + "jni-sys", + "ndk-sys", + "num_enum", + "thiserror", +] + +[[package]] +name = "ndk-context" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b" + +[[package]] +name = "ndk-sys" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e5a6ae77c8ee183dcbbba6150e2e6b9f3f4196a7666c02a715a95692ec1fa97" +dependencies = [ + "jni-sys", +] + +[[package]] +name = "new_debug_unreachable" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" + +[[package]] +name = "nodrop" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72ef4a56884ca558e5ddb05a1d1e7e1bfd9a68d9ed024c21704cc98872dae1bb" + +[[package]] +name = "nu-ansi-term" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" +dependencies = [ + "overload", + "winapi", +] + +[[package]] +name = "num-conv" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf97ec579c3c42f953ef76dbf8d55ac91fb219dde70e49aa4a6b7d74e9919050" + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num_enum" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f646caf906c20226733ed5b1374287eb97e3c2a5c227ce668c1f2ce20ae57c9" +dependencies = [ + "num_enum_derive", +] + +[[package]] +name = "num_enum_derive" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcbff9bc912032c62bf65ef1d5aea88983b420f4f839db1e9b0c281a25c9c799" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "objc" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "915b1b472bc21c53464d6c8461c9d3af805ba1ef837e1cac254428f4a77177b1" +dependencies = [ + "malloc_buf", + "objc_exception", +] + +[[package]] +name = "objc-foundation" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1add1b659e36c9607c7aab864a76c7a4c2760cd0cd2e120f3fb8b952c7e22bf9" +dependencies = [ + "block", + "objc", + "objc_id", +] + +[[package]] +name = "objc_exception" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad970fb455818ad6cba4c122ad012fae53ae8b4795f86378bce65e4f6bab2ca4" +dependencies = [ + "cc", +] + +[[package]] +name = "objc_id" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c92d4ddb4bd7b50d730c215ff871754d0da6b2178849f8a2a2ab69712d0c073b" +dependencies = [ + "objc", +] + +[[package]] +name = "object" +version = "0.36.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27b64972346851a39438c60b341ebc01bba47464ae329e55cf343eb93964efd9" +dependencies = [ + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" + +[[package]] +name = "openssl" +version = "0.10.80" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a45fa2aa886c42762255da344f0a0d313e254066c46aad76f300c3d3da62d967" +dependencies = [ + "bitflags 2.6.0", + "cfg-if", + "foreign-types", + "libc", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.87", +] + +[[package]] +name = "openssl-probe" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" + +[[package]] +name = "openssl-sys" +version = "0.9.116" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f28a22dc7140cda5f096e5e7724a6962ca81a7f8bfd2979f9b18c11af56318c4" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "overload" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" + +[[package]] +name = "pango" +version = "0.15.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22e4045548659aee5313bde6c582b0d83a627b7904dd20dc2d9ef0895d414e4f" +dependencies = [ + "bitflags 1.3.2", + "glib", + "libc", + "once_cell", + "pango-sys", +] + +[[package]] +name = "pango-sys" +version = "0.15.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2a00081cde4661982ed91d80ef437c20eacaf6aa1a5962c0279ae194662c3aa" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps 6.2.2", +] + +[[package]] +name = "parking_lot" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall 0.5.3", + "smallvec", + "windows-targets 0.52.6", +] + +[[package]] +name = "percent-encoding" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" + +[[package]] +name = "phf" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3dfb61232e34fcb633f43d12c58f83c1df82962dcdfa565a4e866ffc17dafe12" +dependencies = [ + "phf_macros 0.8.0", + "phf_shared 0.8.0", + "proc-macro-hack", +] + +[[package]] +name = "phf" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fabbf1ead8a5bcbc20f5f8b939ee3f5b0f6f281b6ad3468b84656b658b455259" +dependencies = [ + "phf_shared 0.10.0", +] + +[[package]] +name = "phf" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ade2d8b8f33c7333b51bcf0428d37e217e9f32192ae4772156f65063b8ce03dc" +dependencies = [ + "phf_macros 0.11.2", + "phf_shared 0.11.2", +] + +[[package]] +name = "phf_codegen" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cbffee61585b0411840d3ece935cce9cb6321f01c45477d30066498cd5e1a815" +dependencies = [ + "phf_generator 0.8.0", + "phf_shared 0.8.0", +] + +[[package]] +name = "phf_codegen" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fb1c3a8bc4dd4e5cfce29b44ffc14bedd2ee294559a294e2a4d4c9e9a6a13cd" +dependencies = [ + "phf_generator 0.10.0", + "phf_shared 0.10.0", +] + +[[package]] +name = "phf_generator" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17367f0cc86f2d25802b2c26ee58a7b23faeccf78a396094c13dced0d0182526" +dependencies = [ + "phf_shared 0.8.0", + "rand 0.7.3", +] + +[[package]] +name = "phf_generator" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d5285893bb5eb82e6aaf5d59ee909a06a16737a8970984dd7746ba9283498d6" +dependencies = [ + "phf_shared 0.10.0", + "rand 0.8.5", +] + +[[package]] +name = "phf_generator" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48e4cc64c2ad9ebe670cb8fd69dd50ae301650392e81c05f9bfcb2d5bdbc24b0" +dependencies = [ + "phf_shared 0.11.2", + "rand 0.8.5", +] + +[[package]] +name = "phf_macros" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f6fde18ff429ffc8fe78e2bf7f8b7a5a5a6e2a8b58bc5a9ac69198bbda9189c" +dependencies = [ + "phf_generator 0.8.0", + "phf_shared 0.8.0", + "proc-macro-hack", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "phf_macros" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3444646e286606587e49f3bcf1679b8cef1dc2c5ecc29ddacaffc305180d464b" +dependencies = [ + "phf_generator 0.11.2", + "phf_shared 0.11.2", + "proc-macro2", + "quote", + "syn 2.0.87", +] + +[[package]] +name = "phf_shared" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c00cf8b9eafe68dde5e9eaa2cef8ee84a9336a47d566ec55ca16589633b65af7" +dependencies = [ + "siphasher", +] + +[[package]] +name = "phf_shared" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6796ad771acdc0123d2a88dc428b5e38ef24456743ddb1744ed628f9815c096" +dependencies = [ + "siphasher", +] + +[[package]] +name = "phf_shared" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90fcb95eef784c2ac79119d1dd819e162b5da872ce6f3c3abe1e8ca1c082f72b" +dependencies = [ + "siphasher", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "pkg-config" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" + +[[package]] +name = "plist" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42cf17e9a1800f5f396bc67d193dc9411b59012a5876445ef450d449881e1016" +dependencies = [ + "base64 0.22.1", + "indexmap 2.3.0", + "quick-xml", + "serde", + "time", +] + +[[package]] +name = "png" +version = "0.17.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06e4b0d3d1312775e782c86c91a111aa1f910cbb65e1337f9975b5f9a554b5e1" +dependencies = [ + "bitflags 1.3.2", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide", +] + +[[package]] +name = "portable-atomic" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05c8b63e8d9609db387f0324918f81d68fe27748f084ef092fb35954d0539a85" + +[[package]] +name = "portable-atomic-util" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a106d1259c23fac8e543272398ae0e3c0b8d33c88ed73d0cc71b0f1d902618" +dependencies = [ + "portable-atomic", +] + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "ppv-lite86" +version = "0.2.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "precomputed-hash" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" + +[[package]] +name = "proc-macro-crate" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919" +dependencies = [ + "once_cell", + "toml_edit 0.19.15", +] + +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn 1.0.109", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + +[[package]] +name = "proc-macro-hack" +version = "0.5.20+deprecated" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc375e1527247fe1a97d8b7156678dfe7c1af2fc075c9a4db3690ecd2a148068" + +[[package]] +name = "proc-macro2" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quick-xml" +version = "0.32.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d3a6e5838b60e0e8fa7a43f22ade549a37d61f8bdbe636d0d7816191de969c2" +dependencies = [ + "memchr", +] + +[[package]] +name = "quote" +version = "1.0.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rand" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03" +dependencies = [ + "getrandom 0.1.16", + "libc", + "rand_chacha 0.2.2", + "rand_core 0.5.1", + "rand_hc", + "rand_pcg", +] + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402" +dependencies = [ + "ppv-lite86", + "rand_core 0.5.1", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_core" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" +dependencies = [ + "getrandom 0.1.16", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.15", +] + +[[package]] +name = "rand_hc" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c" +dependencies = [ + "rand_core 0.5.1", +] + +[[package]] +name = "rand_pcg" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16abd0c1b639e9eb4d7c50c0b8100b0d0f849be2349829c740fe8e6eb4816429" +dependencies = [ + "rand_core 0.5.1", +] + +[[package]] +name = "raw-window-handle" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2ff9a1f06a88b01621b7ae906ef0211290d1c8a168a15542486a8f61c0833b9" + +[[package]] +name = "redox_syscall" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" +dependencies = [ + "bitflags 1.3.2", +] + +[[package]] +name = "redox_syscall" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a908a6e00f1fdd0dfd9c0eb08ce85126f6d8bbda50017e74bc4a4b7d4a926a4" +dependencies = [ + "bitflags 2.6.0", +] + +[[package]] +name = "redox_users" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd283d9651eeda4b2a83a43c1c91b266c40fd76ecd39a50a8c630ae69dc72891" +dependencies = [ + "getrandom 0.2.15", + "libredox", + "thiserror", +] + +[[package]] +name = "ref-cast" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e440fb4e4b4147295338efb76001ab9e4efc0e5839df2c47fc5ac2381d365c3" +dependencies = [ + "ref-cast-impl", +] + +[[package]] +name = "ref-cast-impl" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92ecd8964f8453721699a1ed72037b0db49ce2f5a5138486ee89bed6f67cdf3a" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.4", +] + +[[package]] +name = "regex" +version = "1.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4219d74c6b67a3654a9fbebc4b419e22126d13d2f3c4a07ee0cb61ff79a79619" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata 0.4.7", + "regex-syntax 0.8.4", +] + +[[package]] +name = "regex-automata" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" +dependencies = [ + "regex-syntax 0.6.29", +] + +[[package]] +name = "regex-automata" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38caf58cc5ef2fed281f89292ef23f6365465ed9a41b7a7754eb4e26496c92df" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax 0.8.4", +] + +[[package]] +name = "regex-syntax" +version = "0.6.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" + +[[package]] +name = "regex-syntax" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b" + +[[package]] +name = "reqwest" +version = "0.11.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd67538700a17451e7cba03ac727fb961abb7607553461627b97de0b89cf4a62" +dependencies = [ + "base64 0.21.7", + "bytes", + "encoding_rs", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "hyper", + "hyper-tls", + "ipnet", + "js-sys", + "log", + "mime", + "native-tls", + "once_cell", + "percent-encoding", + "pin-project-lite", + "rustls-pemfile", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "system-configuration", + "tokio", + "tokio-native-tls", + "tokio-util", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-streams", + "web-sys", + "winreg 0.50.0", +] + +[[package]] +name = "rfd" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0149778bd99b6959285b0933288206090c50e2327f47a9c463bfdbf45c8823ea" +dependencies = [ + "block", + "dispatch", + "glib-sys", + "gobject-sys", + "gtk-sys", + "js-sys", + "lazy_static", + "log", + "objc", + "objc-foundation", + "objc_id", + "raw-window-handle", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "windows 0.37.0", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" + +[[package]] +name = "rustc_version" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" +dependencies = [ + "semver", +] + +[[package]] +name = "rustix" +version = "0.38.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70dc5ec042f7a43c4a73241207cecc9873a06d45debb38b329f8541d85c2730f" +dependencies = [ + "bitflags 2.6.0", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.52.0", +] + +[[package]] +name = "rustls-pemfile" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c" +dependencies = [ + "base64 0.21.7", +] + +[[package]] +name = "rustversion" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "955d28af4278de8121b7ebeb796b6a45735dc01436d898801014aced2773a3d6" + +[[package]] +name = "ryu" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "schannel" +version = "0.1.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbc91545643bcf3a0bbb6569265615222618bdf33ce4ffbbd13c4bbd4c093534" +dependencies = [ + "windows-sys 0.52.0", +] + +[[package]] +name = "schemars" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd191f9397d57d581cddd31014772520aa448f65ef991055d7f61582c65165f" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + +[[package]] +name = "schemars" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "687274d293b6cdc6e73e0fee520bf2049650090d7164f87672d212a3c530cf4a" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + +[[package]] +name = "scoped-tls" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "security-framework" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" +dependencies = [ + "bitflags 2.6.0", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75da29fe9b9b08fe9d6b22b5b4bcbc75d8db3aa31e639aa56bb62e9d46bfceaf" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "selectors" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df320f1889ac4ba6bc0cdc9c9af7af4bd64bb927bccdf32d81140dc1f9be12fe" +dependencies = [ + "bitflags 1.3.2", + "cssparser", + "derive_more", + "fxhash", + "log", + "matches", + "phf 0.8.0", + "phf_codegen 0.8.0", + "precomputed-hash", + "servo_arc", + "smallvec", + "thin-slice", +] + +[[package]] +name = "semver" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b" +dependencies = [ + "serde", +] + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.87", +] + +[[package]] +name = "serde_json" +version = "1.0.151" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14" +dependencies = [ + "indexmap 2.3.0", + "itoa 1.0.11", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_repr" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c64451ba24fc7a6a2d60fc75dd9c83c90903b19028d4eff35e88fc1e86564e9" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.87", +] + +[[package]] +name = "serde_spanned" +version = "0.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb5b1b31579f3811bf615c144393417496f152e12ac8b7663bf664f4a815306d" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa 1.0.11", + "ryu", + "serde", +] + +[[package]] +name = "serde_with" +version = "3.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee78f1fbe43ac4a0e47aadb3dbd357b69eb0d3793e948624cd03dd2750ab1c0a" +dependencies = [ + "base64 0.22.1", + "bs58", + "chrono", + "hex", + "indexmap 1.9.3", + "indexmap 2.3.0", + "jiff", + "schemars 0.9.0", + "schemars 1.2.2", + "serde_core", + "serde_json", + "serde_with_macros", + "time", +] + +[[package]] +name = "serde_with_macros" +version = "3.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8705578779c2b6bd90d84d66eb2e206b708b1a4d7b9f17641b293545bf1c7e46" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn 2.0.87", +] + +[[package]] +name = "serialize-to-javascript" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9823f2d3b6a81d98228151fdeaf848206a7855a7a042bbf9bf870449a66cafb" +dependencies = [ + "serde", + "serde_json", + "serialize-to-javascript-impl", +] + +[[package]] +name = "serialize-to-javascript-impl" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74064874e9f6a15f04c1f3cb627902d0e6b410abbf36668afa873c61889f1763" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "servo_arc" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d98238b800e0d1576d8b6e3de32827c2d74bee68bb97748dcf5071fb53965432" +dependencies = [ + "nodrop", + "stable_deref_trait", +] + +[[package]] +name = "sha2" +version = "0.10.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "simd-adler32" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" + +[[package]] +name = "siphasher" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" + +[[package]] +name = "slab" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" +dependencies = [ + "autocfg", +] + +[[package]] +name = "smallvec" +version = "1.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" + +[[package]] +name = "socket2" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce305eb0b4296696835b71df73eb912e0f1ffd2556a501fcede6e0c50349191c" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "soup2" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2b4d76501d8ba387cf0fefbe055c3e0a59891d09f0f995ae4e4b16f6b60f3c0" +dependencies = [ + "bitflags 1.3.2", + "gio", + "glib", + "libc", + "once_cell", + "soup2-sys", +] + +[[package]] +name = "soup2-sys" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "009ef427103fcb17f802871647a7fa6c60cbb654b4c4e4c0ac60a31c5f6dc9cf" +dependencies = [ + "bitflags 1.3.2", + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "system-deps 5.0.0", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" + +[[package]] +name = "state" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbe866e1e51e8260c9eed836a042a5e7f6726bb2b411dffeaa712e19c388f23b" +dependencies = [ + "loom", +] + +[[package]] +name = "string_cache" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f91138e76242f575eb1d3b38b4f1362f10d3a43f47d182a5b359af488a02293b" +dependencies = [ + "new_debug_unreachable", + "once_cell", + "parking_lot", + "phf_shared 0.10.0", + "precomputed-hash", + "serde", +] + +[[package]] +name = "string_cache_codegen" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6bb30289b722be4ff74a408c3cc27edeaad656e06cb1fe8fa9231fa59c728988" +dependencies = [ + "phf_generator 0.10.0", + "phf_shared 0.10.0", + "proc-macro2", + "quote", +] + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.87" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25aa4ce346d03a6dcd68dd8b4010bcb74e54e62c90c573f394c46eae99aba32d" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "3.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6275cddf4610d1775e6d1fe9469b2e77d0f39fd98fb7450901b821e0c53649f" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" + +[[package]] +name = "system-configuration" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7" +dependencies = [ + "bitflags 1.3.2", + "core-foundation", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75fb188eb626b924683e3b95e3a48e63551fcfb51949de2f06a9d91dbee93c9" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "system-deps" +version = "5.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18db855554db7bd0e73e06cf7ba3df39f97812cb11d3f75e71c39bf45171797e" +dependencies = [ + "cfg-expr 0.9.1", + "heck 0.3.3", + "pkg-config", + "toml 0.5.11", + "version-compare 0.0.11", +] + +[[package]] +name = "system-deps" +version = "6.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3e535eb8dded36d55ec13eddacd30dec501792ff23a0b1682c38601b8cf2349" +dependencies = [ + "cfg-expr 0.15.8", + "heck 0.5.0", + "pkg-config", + "toml 0.8.19", + "version-compare 0.2.0", +] + +[[package]] +name = "tao" +version = "0.16.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "575c856fc21e551074869dcfaad8f706412bd5b803dfa0fbf6881c4ff4bfafab" +dependencies = [ + "bitflags 1.3.2", + "cairo-rs", + "cc", + "cocoa", + "core-foundation", + "core-graphics", + "crossbeam-channel", + "dispatch", + "gdk", + "gdk-pixbuf", + "gdk-sys", + "gdkwayland-sys", + "gdkx11-sys", + "gio", + "glib", + "glib-sys", + "gtk", + "image", + "instant", + "jni", + "lazy_static", + "libc", + "log", + "ndk", + "ndk-context", + "ndk-sys", + "objc", + "once_cell", + "parking_lot", + "png", + "raw-window-handle", + "scopeguard", + "serde", + "tao-macros", + "unicode-segmentation", + "uuid", + "windows 0.39.0", + "windows-implement", + "x11-dl", +] + +[[package]] +name = "tao-macros" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec114582505d158b669b136e6851f85840c109819d77c42bb7c0709f727d18c2" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "tar" +version = "0.4.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f6221d9a6003c78398e3b239969f352578258df48c8eb051caadae0015bc840" +dependencies = [ + "filetime", + "libc", + "xattr", +] + +[[package]] +name = "target-lexicon" +version = "0.12.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" + +[[package]] +name = "tauri" +version = "1.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "336bc661a3f3250853fa83c6e5245449ed1c26dce5dcb28bdee7efedf6278806" +dependencies = [ + "anyhow", + "base64 0.21.7", + "bytes", + "cocoa", + "dirs-next", + "dunce", + "embed_plist", + "encoding_rs", + "flate2", + "futures-util", + "getrandom 0.2.15", + "glib", + "glob", + "gtk", + "heck 0.5.0", + "http", + "ignore", + "indexmap 1.9.3", + "minisign-verify", + "objc", + "once_cell", + "percent-encoding", + "rand 0.8.5", + "raw-window-handle", + "reqwest", + "rfd", + "semver", + "serde", + "serde_json", + "serde_repr", + "serialize-to-javascript", + "state", + "tar", + "tauri-macros", + "tauri-runtime", + "tauri-runtime-wry", + "tauri-utils", + "tempfile", + "thiserror", + "time", + "tokio", + "url", + "uuid", + "webkit2gtk", + "webview2-com", + "windows 0.39.0", + "zip", +] + +[[package]] +name = "tauri-build" +version = "1.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0c6ec7a5c3296330c7818478948b422967ce4649094696c985f61d50076d29c" +dependencies = [ + "anyhow", + "cargo_toml", + "dirs-next", + "heck 0.5.0", + "json-patch", + "semver", + "serde", + "serde_json", + "tauri-utils", + "tauri-winres", + "walkdir", +] + +[[package]] +name = "tauri-codegen" +version = "1.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1aed706708ff1200ec12de9cfbf2582b5d8ec05f6a7293911091effbd22036b" +dependencies = [ + "base64 0.21.7", + "brotli", + "ico", + "json-patch", + "plist", + "png", + "proc-macro2", + "quote", + "semver", + "serde", + "serde_json", + "sha2", + "tauri-utils", + "thiserror", + "time", + "uuid", + "walkdir", +] + +[[package]] +name = "tauri-macros" +version = "1.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b88f831d2973ae4f81a706a0004e67dac87f2e4439973bbe98efbd73825d8ede" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "quote", + "syn 1.0.109", + "tauri-codegen", + "tauri-utils", +] + +[[package]] +name = "tauri-runtime" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3068ed62b63dedc705558f4248c7ecbd5561f0f8050949859ea0db2326f26012" +dependencies = [ + "gtk", + "http", + "http-range", + "rand 0.8.5", + "raw-window-handle", + "serde", + "serde_json", + "tauri-utils", + "thiserror", + "url", + "uuid", + "webview2-com", + "windows 0.39.0", +] + +[[package]] +name = "tauri-runtime-wry" +version = "0.14.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4c3db170233096aa30330feadcd895bf9317be97e624458560a20e814db7955" +dependencies = [ + "cocoa", + "gtk", + "percent-encoding", + "rand 0.8.5", + "raw-window-handle", + "tauri-runtime", + "tauri-utils", + "uuid", + "webkit2gtk", + "webview2-com", + "windows 0.39.0", + "wry", +] + +[[package]] +name = "tauri-utils" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2826db448309d382dac14d520f0c0a40839b87b57b977e59cf5f296b3ace6a93" +dependencies = [ + "brotli", + "ctor", + "dunce", + "glob", + "heck 0.5.0", + "html5ever", + "infer", + "json-patch", + "kuchikiki", + "log", + "memchr", + "phf 0.11.2", + "proc-macro2", + "quote", + "semver", + "serde", + "serde_json", + "serde_with", + "thiserror", + "url", + "walkdir", + "windows-version", +] + +[[package]] +name = "tauri-winres" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5993dc129e544393574288923d1ec447c857f3f644187f4fbf7d9a875fbfc4fb" +dependencies = [ + "embed-resource", + "toml 0.7.8", +] + +[[package]] +name = "tempfile" +version = "3.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04cbcdd0c794ebb0d4cf35e88edd2f7d2c4c3e9a5a6dab322839b321c6a87a64" +dependencies = [ + "cfg-if", + "fastrand", + "once_cell", + "rustix", + "windows-sys 0.59.0", +] + +[[package]] +name = "tendril" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d24a120c5fc464a3458240ee02c299ebcb9d67b5249c8848b09d639dca8d7bb0" +dependencies = [ + "futf", + "mac", + "utf-8", +] + +[[package]] +name = "thin-slice" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8eaa81235c7058867fa8c0e7314f33dcce9c215f535d1913822a2b3f5e289f3c" + +[[package]] +name = "thiserror" +version = "1.0.63" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0342370b38b6a11b6cc11d6a805569958d54cfa061a29969c3b5ce2ea405724" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.63" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4558b58466b9ad7ca0f102865eccc95938dca1a74a856f2b57b6629050da261" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.87", +] + +[[package]] +name = "thread_local" +version = "1.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c" +dependencies = [ + "cfg-if", + "once_cell", +] + +[[package]] +name = "time" +version = "0.3.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" +dependencies = [ + "deranged", + "itoa 1.0.11", + "num-conv", + "powerfmt", + "serde_core", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" + +[[package]] +name = "time-macros" +version = "0.2.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "tiny_http" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0d6ef4e10d23c1efb862eecad25c5054429a71958b4eeef85eb5e7170b477ca" +dependencies = [ + "ascii", + "chunked_transfer", + "log", + "time", + "url", +] + +[[package]] +name = "tinyvec" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "445e881f4f6d382d5f27c034e25eb92edd7c784ceab92a0937db7f2e9471b938" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tokio" +version = "1.39.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daa4fb1bc778bd6f04cbfc4bb2d06a7396a8f299dc33ea1900cedaa316f467b1" +dependencies = [ + "backtrace", + "bytes", + "libc", + "mio", + "pin-project-lite", + "socket2", + "windows-sys 0.52.0", +] + +[[package]] +name = "tokio-native-tls" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" +dependencies = [ + "native-tls", + "tokio", +] + +[[package]] +name = "tokio-util" +version = "0.7.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cf6b47b3771c49ac75ad09a6162f53ad4b8088b76ac60e8ec1455b31a189fe1" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "toml" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4f7f0dd8d50a853a531c426359045b1998f04219d88799810762cd4ad314234" +dependencies = [ + "serde", +] + +[[package]] +name = "toml" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd79e69d3b627db300ff956027cc6c3798cef26d22526befdfcd12feeb6d2257" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit 0.19.15", +] + +[[package]] +name = "toml" +version = "0.8.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1ed1f98e3fdc28d6d910e6737ae6ab1a93bf1985935a1193e68f93eeb68d24e" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit 0.22.20", +] + +[[package]] +name = "toml_datetime" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_edit" +version = "0.19.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" +dependencies = [ + "indexmap 2.3.0", + "serde", + "serde_spanned", + "toml_datetime", + "winnow 0.5.40", +] + +[[package]] +name = "toml_edit" +version = "0.22.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "583c44c02ad26b0c3f3066fe629275e50627026c51ac2e595cca4c230ce1ce1d" +dependencies = [ + "indexmap 2.3.0", + "serde", + "serde_spanned", + "toml_datetime", + "winnow 0.6.18", +] + +[[package]] +name = "tower-service" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52" + +[[package]] +name = "tracing" +version = "0.1.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" +dependencies = [ + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.87", +] + +[[package]] +name = "tracing-core" +version = "0.1.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad0f048c97dbd9faa9b7df56362b8ebcaa52adb06b498c050d2f4e32f90a7a8b" +dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "typenum" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" + +[[package]] +name = "unicode-bidi" +version = "0.3.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08f95100a766bf4f8f28f90d77e0a5461bbdb219042e7679bebe79004fed8d75" + +[[package]] +name = "unicode-ident" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" + +[[package]] +name = "unicode-normalization" +version = "0.1.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a56d1686db2308d901306f92a263857ef59ea39678a5458e7cb17f01415101f5" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "unicode-segmentation" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4c87d22b6e3f4a18d4d40ef354e97c90fcb14dd91d7dc0aa9d8a1172ebf7202" + +[[package]] +name = "url" +version = "2.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22784dbdf76fdde8af1aeda5622b546b422b6fc585325248a2bf9f5e41e94d6c" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "utf-8" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" + +[[package]] +name = "uuid" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81dfa00651efa65069b0b6b651f4aaa31ba9e3c3ce0137aaad053604ee7e0314" +dependencies = [ + "getrandom 0.2.15", +] + +[[package]] +name = "valuable" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "version-compare" +version = "0.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c18c859eead79d8b95d09e4678566e8d70105c4e7b251f707a03df32442661b" + +[[package]] +name = "version-compare" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "852e951cb7832cb45cb1169900d19760cfa39b82bc0ea9c0e5a14ae88411c98b" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "vswhom" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be979b7f07507105799e854203b470ff7c78a1639e330a58f183b5fea574608b" +dependencies = [ + "libc", + "vswhom-sys", +] + +[[package]] +name = "vswhom-sys" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3b17ae1f6c8a2b28506cd96d412eebf83b4a0ff2cbefeeb952f2f9dfa44ba18" +dependencies = [ + "cc", + "libc", +] + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.9.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "wasm-bindgen" +version = "0.2.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4be2531df63900aeb2bca0daaaddec08491ee64ceecbee5076636a3b026795a8" +dependencies = [ + "cfg-if", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "614d787b966d3989fa7bb98a654e369c762374fd3213d212cfc0251257e747da" +dependencies = [ + "bumpalo", + "log", + "once_cell", + "proc-macro2", + "quote", + "syn 2.0.87", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76bc14366121efc8dbb487ab05bcc9d346b3b5ec0eaa76e46594cabbe51762c0" +dependencies = [ + "cfg-if", + "js-sys", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1f8823de937b71b9460c0c34e25f3da88250760bec0ebac694b49997550d726" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.87", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96" + +[[package]] +name = "wasm-streams" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b65dc4c90b63b118468cf747d8bf3566c1913ef60be765b5730ead9e0a3ba129" +dependencies = [ + "futures-util", + "js-sys", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "web-sys" +version = "0.3.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77afa9a11836342370f4817622a2f0f418b134426d91a82dfb48f532d2ec13ef" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "webkit2gtk" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8f859735e4a452aeb28c6c56a852967a8a76c8eb1cc32dbf931ad28a13d6370" +dependencies = [ + "bitflags 1.3.2", + "cairo-rs", + "gdk", + "gdk-sys", + "gio", + "gio-sys", + "glib", + "glib-sys", + "gobject-sys", + "gtk", + "gtk-sys", + "javascriptcore-rs", + "libc", + "once_cell", + "soup2", + "webkit2gtk-sys", +] + +[[package]] +name = "webkit2gtk-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d76ca6ecc47aeba01ec61e480139dda143796abcae6f83bcddf50d6b5b1dcf3" +dependencies = [ + "atk-sys", + "bitflags 1.3.2", + "cairo-sys-rs", + "gdk-pixbuf-sys", + "gdk-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "gtk-sys", + "javascriptcore-rs-sys", + "libc", + "pango-sys", + "pkg-config", + "soup2-sys", + "system-deps 6.2.2", +] + +[[package]] +name = "webview2-com" +version = "0.19.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4a769c9f1a64a8734bde70caafac2b96cada12cd4aefa49196b3a386b8b4178" +dependencies = [ + "webview2-com-macros", + "webview2-com-sys", + "windows 0.39.0", + "windows-implement", +] + +[[package]] +name = "webview2-com-macros" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eaebe196c01691db62e9e4ca52c5ef1e4fd837dcae27dae3ada599b5a8fd05ac" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "webview2-com-sys" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aac48ef20ddf657755fdcda8dfed2a7b4fc7e4581acce6fe9b88c3d64f29dee7" +dependencies = [ + "regex", + "serde", + "serde_json", + "thiserror", + "windows 0.39.0", + "windows-bindgen", + "windows-metadata", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" +dependencies = [ + "windows-sys 0.48.0", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57b543186b344cc61c85b5aab0d2e3adf4e0f99bc076eff9aa5927bcc0b8a647" +dependencies = [ + "windows_aarch64_msvc 0.37.0", + "windows_i686_gnu 0.37.0", + "windows_i686_msvc 0.37.0", + "windows_x86_64_gnu 0.37.0", + "windows_x86_64_msvc 0.37.0", +] + +[[package]] +name = "windows" +version = "0.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1c4bd0a50ac6020f65184721f758dba47bb9fbc2133df715ec74a237b26794a" +dependencies = [ + "windows-implement", + "windows_aarch64_msvc 0.39.0", + "windows_i686_gnu 0.39.0", + "windows_i686_msvc 0.39.0", + "windows_x86_64_gnu 0.39.0", + "windows_x86_64_msvc 0.39.0", +] + +[[package]] +name = "windows" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e686886bc078bc1b0b600cac0147aadb815089b6e4da64016cbd754b6342700f" +dependencies = [ + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-bindgen" +version = "0.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68003dbd0e38abc0fb85b939240f4bce37c43a5981d3df37ccbaaa981b47cb41" +dependencies = [ + "windows-metadata", + "windows-tokens", +] + +[[package]] +name = "windows-core" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-implement" +version = "0.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba01f98f509cb5dc05f4e5fc95e535f78260f15fea8fe1a8abdd08f774f1cee7" +dependencies = [ + "syn 1.0.109", + "windows-tokens", +] + +[[package]] +name = "windows-metadata" +version = "0.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ee5e275231f07c6e240d14f34e1b635bf1faa1c76c57cfd59a5cdb9848e4278" + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows-tokens" +version = "0.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f838de2fe15fe6bac988e74b798f26499a8b21a9d97edec321e79b28d1d7f597" + +[[package]] +name = "windows-version" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6998aa457c9ba8ff2fb9f13e9d2a930dabcea28f1d0ab94d687d8b3654844515" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2623277cb2d1c216ba3b578c0f3cf9cdebeddb6e66b1b218bb33596ea7769c3a" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec7711666096bd4096ffa835238905bb33fb87267910e154b18b44eaabb340f2" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3925fd0b0b804730d44d4b6278c50f9699703ec49bcd628020f46f4ba07d9e1" + +[[package]] +name = "windows_i686_gnu" +version = "0.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "763fc57100a5f7042e3057e7e8d9bdd7860d330070251a73d003563a3bb49e1b" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce907ac74fe331b524c1298683efbf598bb031bc84d5e274db2083696d07c57c" + +[[package]] +name = "windows_i686_msvc" +version = "0.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7bc7cbfe58828921e10a9f446fcaaf649204dcfe6c1ddd712c5eebae6bda1106" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2babfba0828f2e6b32457d5341427dcbb577ceef556273229959ac23a10af33d" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6868c165637d653ae1e8dc4d82c25d4f97dd6605eaa8d784b5c6e0ab2a252b65" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4dd6dc7df2d84cf7b33822ed5b86318fb1781948e9663bacd047fc9dd52259d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e4d40883ae9cae962787ca76ba76390ffa29214667a111db9e0a1ad8377e809" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "winnow" +version = "0.5.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876" +dependencies = [ + "memchr", +] + +[[package]] +name = "winnow" +version = "0.6.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68a9bda4691f099d435ad181000724da8e5899daa10713c2d432552b9ccd3a6f" +dependencies = [ + "memchr", +] + +[[package]] +name = "winreg" +version = "0.50.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1" +dependencies = [ + "cfg-if", + "windows-sys 0.48.0", +] + +[[package]] +name = "winreg" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a277a57398d4bfa075df44f501a17cfdf8542d224f0d36095a2adc7aee4ef0a5" +dependencies = [ + "cfg-if", + "windows-sys 0.48.0", +] + +[[package]] +name = "wry" +version = "0.24.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a2a144c3ab5e83e04724bc8e67cea552ffae413185fda459fafdae173fd985d" +dependencies = [ + "base64 0.13.1", + "block", + "cocoa", + "core-graphics", + "crossbeam-channel", + "dunce", + "gdk", + "gio", + "glib", + "gtk", + "html5ever", + "http", + "kuchikiki", + "libc", + "log", + "objc", + "objc_id", + "once_cell", + "serde", + "serde_json", + "sha2", + "soup2", + "tao", + "thiserror", + "url", + "webkit2gtk", + "webkit2gtk-sys", + "webview2-com", + "windows 0.39.0", + "windows-implement", +] + +[[package]] +name = "x11" +version = "2.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "502da5464ccd04011667b11c435cb992822c2c0dbde1770c988480d312a0db2e" +dependencies = [ + "libc", + "pkg-config", +] + +[[package]] +name = "x11-dl" +version = "2.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38735924fedd5314a6e548792904ed8c6de6636285cb9fec04d5b1db85c1516f" +dependencies = [ + "libc", + "once_cell", + "pkg-config", +] + +[[package]] +name = "xattr" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8da84f1a25939b27f6820d92aed108f83ff920fdf11a7b19366c27c4cda81d4f" +dependencies = [ + "libc", + "linux-raw-sys", + "rustix", +] + +[[package]] +name = "zerocopy" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" +dependencies = [ + "byteorder", + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.87", +] + +[[package]] +name = "zip" +version = "0.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "760394e246e4c28189f19d488c058bf16f564016aefac5d32bb1f3b51d5e9261" +dependencies = [ + "byteorder", + "crc32fast", + "crossbeam-utils", +] + +[[package]] +name = "zmij" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" diff --git a/plugins/updater/tests/updater-migration/v1-app/Cargo.toml b/plugins/updater/tests/updater-migration/v1-app/Cargo.toml new file mode 100644 index 0000000..e99e410 --- /dev/null +++ b/plugins/updater/tests/updater-migration/v1-app/Cargo.toml @@ -0,0 +1,18 @@ +workspace = {} + +[package] +name = "app-updater-v1" +version = "0.1.0" +edition = "2021" + +[build-dependencies] +tauri-build = { version = "1", features = [] } + +[dependencies] +serde = { version = "1", features = ["derive"] } +serde_json = "1" +tiny_http = "0.11" +tauri = { version = "1", features = ["updater"] } + +[features] +custom-protocol = ["tauri/custom-protocol"] diff --git a/plugins/updater/tests/updater-migration/v1-app/build.rs b/plugins/updater/tests/updater-migration/v1-app/build.rs new file mode 100644 index 0000000..5ebf8d2 --- /dev/null +++ b/plugins/updater/tests/updater-migration/v1-app/build.rs @@ -0,0 +1,7 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +fn main() { + tauri_build::build() +} diff --git a/plugins/updater/tests/updater-migration/v1-app/package.json b/plugins/updater/tests/updater-migration/v1-app/package.json new file mode 100644 index 0000000..2379b09 --- /dev/null +++ b/plugins/updater/tests/updater-migration/v1-app/package.json @@ -0,0 +1,10 @@ +{ + "name": "v1-app", + "version": "0.0.0", + "dependencies": { + "@tauri-apps/cli": "^1.0.0" + }, + "scripts": { + "tauri": "tauri" + } +} diff --git a/plugins/updater/tests/updater-migration/v1-app/src/main.rs b/plugins/updater/tests/updater-migration/v1-app/src/main.rs new file mode 100644 index 0000000..3c3138d --- /dev/null +++ b/plugins/updater/tests/updater-migration/v1-app/src/main.rs @@ -0,0 +1,50 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] + +fn main() { + let mut context = tauri::generate_context!(); + if std::env::var("TARGET").unwrap_or_default() == "nsis" { + // /D sets the default installation directory ($INSTDIR), + // overriding InstallDir and InstallDirRegKey. + // It must be the last parameter used in the command line and must not contain any quotes, even if the path contains spaces. + // Only absolute paths are supported. + // NOTE: we only need this because this is an integration test and we don't want to install the app in the programs folder + context.config_mut().tauri.updater.windows.installer_args = vec![format!( + "/D={}", + tauri::utils::platform::current_exe() + .unwrap() + .parent() + .unwrap() + .display() + )]; + } + tauri::Builder::default() + .setup(|app| { + println!("version={}", app.package_info().version); + let handle = app.handle(); + tauri::async_runtime::spawn(async move { + match handle.updater().check().await { + Ok(update) => { + if update.is_update_available() { + if let Err(e) = update.download_and_install().await { + println!("{e}"); + std::process::exit(1); + } + std::process::exit(0); + } + std::process::exit(2); + } + Err(e) => { + println!("{e}"); + std::process::exit(1); + } + } + }); + Ok(()) + }) + .run(context) + .expect("error while running tauri application"); +} diff --git a/plugins/updater/tests/updater-migration/v1-app/tauri.conf.json b/plugins/updater/tests/updater-migration/v1-app/tauri.conf.json new file mode 100644 index 0000000..13e7988 --- /dev/null +++ b/plugins/updater/tests/updater-migration/v1-app/tauri.conf.json @@ -0,0 +1,39 @@ +{ + "$schema": "../../../core/tauri-config-schema/schema.json", + "build": { + "distDir": [], + "devPath": [] + }, + "tauri": { + "bundle": { + "active": true, + "targets": "all", + "identifier": "com.tauri.updater", + "icon": [ + "../icons/32x32.png", + "../icons/128x128.png", + "../icons/128x128@2x.png", + "../icons/icon.icns", + "../icons/icon.ico" + ], + "category": "DeveloperTool", + "windows": { + "wix": { + "skipWebviewInstall": true + } + } + }, + "allowlist": { + "all": false + }, + "updater": { + "active": true, + "dialog": false, + "pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IEUwNDRGMjkwRjg2MDhCRDAKUldUUWkyRDRrUEpFNEQ4SmdwcU5PaXl6R2ZRUUNvUnhIaVkwVUltV0NMaEx6VTkrWVhpT0ZqeEEK", + "endpoints": ["http://localhost:3007"], + "windows": { + "installMode": "quiet" + } + } + } +} diff --git a/plugins/updater/tests/updater-migration/v2-app/.gitignore b/plugins/updater/tests/updater-migration/v2-app/.gitignore new file mode 100644 index 0000000..6a4bb53 --- /dev/null +++ b/plugins/updater/tests/updater-migration/v2-app/.gitignore @@ -0,0 +1 @@ +/gen/schemas diff --git a/plugins/updater/tests/updater-migration/v2-app/Cargo.toml b/plugins/updater/tests/updater-migration/v2-app/Cargo.toml new file mode 100644 index 0000000..69d60c4 --- /dev/null +++ b/plugins/updater/tests/updater-migration/v2-app/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "app-updater-v2" +version = "0.1.0" +edition = { workspace = true } + +[build-dependencies] +tauri-build = { workspace = true } + +[dependencies] +tauri = { workspace = true, features = ["wry", "common-controls-v6", "x11"] } +serde = { workspace = true } +serde_json = { workspace = true } +tauri-plugin-updater = { path = "../../.." } +tiny_http = "0.12" diff --git a/plugins/updater/tests/updater-migration/v2-app/build.rs b/plugins/updater/tests/updater-migration/v2-app/build.rs new file mode 100644 index 0000000..5ebf8d2 --- /dev/null +++ b/plugins/updater/tests/updater-migration/v2-app/build.rs @@ -0,0 +1,7 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +fn main() { + tauri_build::build() +} diff --git a/plugins/updater/tests/updater-migration/v2-app/src/main.rs b/plugins/updater/tests/updater-migration/v2-app/src/main.rs new file mode 100644 index 0000000..b2bbe04 --- /dev/null +++ b/plugins/updater/tests/updater-migration/v2-app/src/main.rs @@ -0,0 +1,58 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] + +use tauri_plugin_updater::UpdaterExt; + +fn main() { + #[allow(unused_mut)] + let mut context = tauri::generate_context!(); + + tauri::Builder::default() + .plugin(tauri_plugin_updater::Builder::new().build()) + .setup(|app| { + println!("version={}", app.package_info().version); + let handle = app.handle().clone(); + tauri::async_runtime::spawn(async move { + let mut builder = handle.updater_builder(); + if std::env::var("TARGET").unwrap_or_default() == "nsis" { + // /D sets the default installation directory ($INSTDIR), + // overriding InstallDir and InstallDirRegKey. + // It must be the last parameter used in the command line and must not contain any quotes, even if the path contains spaces. + // Only absolute paths are supported. + // NOTE: we only need this because this is an integration test and we don't want to install the app in the programs folder + builder = builder.installer_args(vec![format!( + "/D={}", + tauri::utils::platform::current_exe() + .unwrap() + .parent() + .unwrap() + .display() + )]); + } + let updater = builder.build().unwrap(); + + match updater.check().await { + Ok(Some(update)) => { + if let Err(e) = update.download_and_install(|_, _| {}, || {}).await { + println!("{e}"); + std::process::exit(1); + } + std::process::exit(0); + } + Ok(None) => { + std::process::exit(2); + } + Err(e) => { + println!("{e}"); + std::process::exit(1); + } + } + }); + Ok(()) + }) + .run(context) + .expect("error while running tauri application"); +} diff --git a/plugins/updater/tests/updater-migration/v2-app/tauri.conf.json b/plugins/updater/tests/updater-migration/v2-app/tauri.conf.json new file mode 100644 index 0000000..d6cff85 --- /dev/null +++ b/plugins/updater/tests/updater-migration/v2-app/tauri.conf.json @@ -0,0 +1,33 @@ +{ + "identifier": "com.tauri.updater", + "plugins": { + "updater": { + "endpoints": ["http://localhost:3007"], + "pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IEUwNDRGMjkwRjg2MDhCRDAKUldUUWkyRDRrUEpFNEQ4SmdwcU5PaXl6R2ZRUUNvUnhIaVkwVUltV0NMaEx6VTkrWVhpT0ZqeEEK", + "windows": { + "installMode": "quiet", + "installerArgs": ["/NS"] + } + } + }, + "bundle": { + "active": true, + "targets": "all", + "createUpdaterArtifacts": true, + "icon": [ + "../icons/32x32.png", + "../icons/128x128.png", + "../icons/128x128@2x.png", + "../icons/icon.icns", + "../icons/icon.ico" + ], + "windows": { + "webviewInstallMode": { + "type": "skip" + }, + "nsis": { + "compression": "none" + } + } + } +} diff --git a/plugins/updater/tsconfig.json b/plugins/updater/tsconfig.json new file mode 100644 index 0000000..4374468 --- /dev/null +++ b/plugins/updater/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "rootDir": "guest-js" + }, + "include": ["guest-js/*.ts"] +} diff --git a/plugins/upload/CHANGELOG.md b/plugins/upload/CHANGELOG.md new file mode 100644 index 0000000..ccd5dc3 --- /dev/null +++ b/plugins/upload/CHANGELOG.md @@ -0,0 +1,137 @@ +# Changelog + +## [2.4.1] + +- [`f8053e65`](https://github.com/tauri-apps/plugins-workspace/commit/f8053e659e4ccd85c1f52833411ff8417cbc5e69) ([#3527](https://github.com/tauri-apps/plugins-workspace/pull/3527) by [@Legend-Master](https://github.com/tauri-apps/plugins-workspace/../../Legend-Master)) Documented Cargo feature flags in each plugin's crate-level documentation. + +## \[2.4.0] + +- [`ad910b11`](https://github.com/tauri-apps/plugins-workspace/commit/ad910b1135d5cb57a67ca022ae6beb0dca460f9c) ([#2991](https://github.com/tauri-apps/plugins-workspace/pull/2991) by [@velocitysystems](https://github.com/tauri-apps/plugins-workspace/../../velocitysystems)) Upload plugin now supports specifying an HTTP method i.e. POST, PUT etc. + +## \[2.3.2] + +- [`93426f85`](https://github.com/tauri-apps/plugins-workspace/commit/93426f85120f49beb9f40222bff45185a32d54a9) Fixed an issue that caused docs.rs builds to fail. No user facing changes. + +## \[2.3.1] + +- [`b7292030`](https://github.com/tauri-apps/plugins-workspace/commit/b7292030594daa04e78979214478031241b6e38e) ([#2838](https://github.com/tauri-apps/plugins-workspace/pull/2838) by [@velocitysystems](https://github.com/tauri-apps/plugins-workspace/../../velocitysystems)) Fix `download` and `upload` locks main thread on Android. + Use Tokio to spawn task when invoking commands. + +## \[2.3.0] + +- [`f209b2f2`](https://github.com/tauri-apps/plugins-workspace/commit/f209b2f23cb29133c97ad5961fb46ef794dbe063) ([#2804](https://github.com/tauri-apps/plugins-workspace/pull/2804) by [@renovate](https://github.com/tauri-apps/plugins-workspace/../../renovate)) Updated tauri to 2.6 + +## \[2.2.2] + +- [`f634e524`](https://github.com/tauri-apps/plugins-workspace/commit/f634e5248ebe428f8305a59f74c13fc15147fb8e) This is an "empty" release to update the plugins' source files on crates.io and docs.rs. This should fix docs.rs build failures for projects using tauri plugins as dependencies. + +## \[2.2.1] + +- [`ca7395a5`](https://github.com/tauri-apps/plugins-workspace/commit/ca7395a5ce971cbac25fb54285056edf3dd84e1f) ([#2378](https://github.com/tauri-apps/plugins-workspace/pull/2378) by [@FabianLars](https://github.com/tauri-apps/plugins-workspace/../../FabianLars)) the `rustls-tls` feature is now enabled by default + +## \[2.2.1] + +- [`05c62d73`](https://github.com/tauri-apps/plugins-workspace/commit/05c62d731fa48fd06b8cb3694a962d8cb0db8619) ([#1523](https://github.com/tauri-apps/plugins-workspace/pull/1523) by [@enri90](https://github.com/tauri-apps/plugins-workspace/../../enri90)) Added post request on download function + +## \[2.2.0] + +- [`5dadd205`](https://github.com/tauri-apps/plugins-workspace/commit/5dadd205f5596c9607457dd015a2f9d7fc48a2db) ([#2033](https://github.com/tauri-apps/plugins-workspace/pull/2033) by [@514sid](https://github.com/tauri-apps/plugins-workspace/../../514sid)) Added a new field `progressTotal` to track the total amount of data transferred during the upload/download process. + +## \[2.1.0] + +- [`87cc5852`](https://github.com/tauri-apps/plugins-workspace/commit/87cc58527d769960427a2f46bb10532f5dcf7ace) ([#1797](https://github.com/tauri-apps/plugins-workspace/pull/1797) by [@VirtualPirate](https://github.com/tauri-apps/plugins-workspace/../../VirtualPirate)) Added feature for calculating `transfer_speed` during file uploads and downloads + +## \[2.0.1] + +- [`a1a82208`](https://github.com/tauri-apps/plugins-workspace/commit/a1a82208ed4ab87f83310be0dc95428aec9ab241) ([#1873](https://github.com/tauri-apps/plugins-workspace/pull/1873) by [@lucasfernog](https://github.com/tauri-apps/plugins-workspace/../../lucasfernog)) Downgrade MSRV to 1.77.2 to support Windows 7. + +## \[2.0.0] + +- [`e2c4dfb6`](https://github.com/tauri-apps/plugins-workspace/commit/e2c4dfb6af43e5dd8d9ceba232c315f5febd55c1) Update to tauri v2 stable release. + +## \[2.0.0-rc.2] + +### bug + +- [`1d9741b5`](https://github.com/tauri-apps/plugins-workspace/commit/1d9741b52bb242d32b2ffd46fb4343a501cbd54b) ([#1783](https://github.com/tauri-apps/plugins-workspace/pull/1783) by [@fxsalazar](https://github.com/tauri-apps/plugins-workspace/../../fxsalazar)) fix download content to file when unsuccessful response + +## \[2.0.0-rc.1] + +- [`e2e97db5`](https://github.com/tauri-apps/plugins-workspace/commit/e2e97db51983267f5be84d4f6f0278d58834d1f5) ([#1701](https://github.com/tauri-apps/plugins-workspace/pull/1701) by [@lucasfernog](https://github.com/tauri-apps/plugins-workspace/../../lucasfernog)) Update to tauri 2.0.0-rc.8 + +## \[2.0.0-rc.0] + +- [`9887d1`](https://github.com/tauri-apps/plugins-workspace/commit/9887d14bd0e971c4c0f5c1188fc4005d3fc2e29e) Update to tauri RC. + +## \[2.0.0-beta.9] + +- [`99d6ac0f`](https://github.com/tauri-apps/plugins-workspace/commit/99d6ac0f9506a6a4a1aa59c728157190a7441af6) ([#1606](https://github.com/tauri-apps/plugins-workspace/pull/1606) by [@FabianLars](https://github.com/tauri-apps/plugins-workspace/../../FabianLars)) The JS packages now specify the *minimum* `@tauri-apps/api` version instead of a single exact version. +- [`6de87966`](https://github.com/tauri-apps/plugins-workspace/commit/6de87966ecc00ad9d91c25be452f1f46bd2b7e1f) ([#1597](https://github.com/tauri-apps/plugins-workspace/pull/1597) by [@Legend-Master](https://github.com/tauri-apps/plugins-workspace/../../Legend-Master)) Update to tauri beta.25. + +## \[2.0.0-beta.8] + +- [`22a17980`](https://github.com/tauri-apps/plugins-workspace/commit/22a17980ff4f6f8c40adb1b8f4ffc6dae2fe7e30) ([#1537](https://github.com/tauri-apps/plugins-workspace/pull/1537) by [@lucasfernog](https://github.com/tauri-apps/plugins-workspace/../../lucasfernog)) Update to tauri beta.24. + +## \[2.0.0-beta.7] + +- [`76daee7a`](https://github.com/tauri-apps/plugins-workspace/commit/76daee7aafece34de3092c86e531cf9eb1138989) ([#1512](https://github.com/tauri-apps/plugins-workspace/pull/1512) by [@renovate](https://github.com/tauri-apps/plugins-workspace/../../renovate)) Update to tauri beta.23. + +## \[2.0.0-beta.6] + +- [`9013854f`](https://github.com/tauri-apps/plugins-workspace/commit/9013854f42a49a230b9dbb9d02774765528a923f)([#1382](https://github.com/tauri-apps/plugins-workspace/pull/1382)) Update to tauri beta.22. + +## \[2.0.0-beta.5] + +- [`430bd6f4`](https://github.com/tauri-apps/plugins-workspace/commit/430bd6f4f379bee5d232ae6b098ae131db7f178a)([#1363](https://github.com/tauri-apps/plugins-workspace/pull/1363)) Update to tauri beta.20. + +## \[2.0.0-beta.4] + +- [`bd1ed590`](https://github.com/tauri-apps/plugins-workspace/commit/bd1ed5903ffcce5500310dac1e59e8c67674ef1e)([#1237](https://github.com/tauri-apps/plugins-workspace/pull/1237)) Update to tauri beta.17. + +## \[2.0.0-beta.3] + +- [`4a5ab18`](https://github.com/tauri-apps/plugins-workspace/commit/4a5ab18a229b902314f242d656b3a2290a8b9065)([#976](https://github.com/tauri-apps/plugins-workspace/pull/976)) Return the upload response as a string and error out if the status code is not within 200-299. + +## \[2.0.0-beta.3] + +- [`a04ea2f`](https://github.com/tauri-apps/plugins-workspace/commit/a04ea2f38294d5a3987578283badc8eec87a7752)([#1071](https://github.com/tauri-apps/plugins-workspace/pull/1071)) The global API script is now only added to the binary when the `withGlobalTauri` config is true. + +## \[2.0.0-beta.2] + +- [`99bea25`](https://github.com/tauri-apps/plugins-workspace/commit/99bea2559c2c0648c2519c50a18cd124dacef57b)([#1005](https://github.com/tauri-apps/plugins-workspace/pull/1005)) Update to tauri beta.8. + +## \[2.0.0-beta.1] + +- [`569defb`](https://github.com/tauri-apps/plugins-workspace/commit/569defbe9492e38938554bb7bdc1be9151456d21) Update to tauri beta.4. + +## \[2.0.0-beta.0] + +- [`d198c01`](https://github.com/tauri-apps/plugins-workspace/commit/d198c014863ee260cb0de88a14b7fc4356ef7474)([#862](https://github.com/tauri-apps/plugins-workspace/pull/862)) Update to tauri beta. + +## \[2.0.0-alpha.5] + +- [`387c2f9`](https://github.com/tauri-apps/plugins-workspace/commit/387c2f9e0ce4c75c07ffa3fd76391a25b58f5daf)([#802](https://github.com/tauri-apps/plugins-workspace/pull/802)) Update to @tauri-apps/api v2.0.0-alpha.13. + +## \[2.0.0-alpha.4] + +- [`387c2f9`](https://github.com/tauri-apps/plugins-workspace/commit/387c2f9e0ce4c75c07ffa3fd76391a25b58f5daf)([#802](https://github.com/tauri-apps/plugins-workspace/pull/802)) Update to @tauri-apps/api v2.0.0-alpha.12. + +## \[2.0.0-alpha.3] + +- [`e438e0a`](https://github.com/tauri-apps/plugins-workspace/commit/e438e0a62d4b430a5159f05f13ecd397dd891a0d)([#676](https://github.com/tauri-apps/plugins-workspace/pull/676)) Update to @tauri-apps/api v2.0.0-alpha.11. + +## \[2.0.0-alpha.2] + +- [`5c13736`](https://github.com/tauri-apps/plugins-workspace/commit/5c137365c60790e8d4037d449e8237aa3fffdab0)([#673](https://github.com/tauri-apps/plugins-workspace/pull/673)) Update to @tauri-apps/api v2.0.0-alpha.9. + +## \[2.0.0-alpha.2] + +- [`4e2cef9`](https://github.com/tauri-apps/plugins-workspace/commit/4e2cef9b702bbbb9cf4ee17de50791cb21f1b2a4)([#593](https://github.com/tauri-apps/plugins-workspace/pull/593)) Update to alpha.12. + +## \[2.0.0-alpha.1] + +- [`d74fc0a`](https://github.com/tauri-apps/plugins-workspace/commit/d74fc0a097996e90a37be8f57d50b7d1f6ca616f)([#555](https://github.com/tauri-apps/plugins-workspace/pull/555)) Update to alpha.11. + +## \[2.0.0-alpha.0] + +- [`717ae67`](https://github.com/tauri-apps/plugins-workspace/commit/717ae670978feb4492fac1f295998b93f2b9347f)([#371](https://github.com/tauri-apps/plugins-workspace/pull/371)) First v2 alpha release! diff --git a/plugins/upload/Cargo.toml b/plugins/upload/Cargo.toml new file mode 100644 index 0000000..c46c6d2 --- /dev/null +++ b/plugins/upload/Cargo.toml @@ -0,0 +1,54 @@ +[package] +name = "tauri-plugin-upload" +version = "2.4.1" +description = "Upload files from disk to a remote server over HTTP." +authors = { workspace = true } +license = { workspace = true } +edition = { workspace = true } +rust-version = { workspace = true } +repository = { workspace = true } +links = "tauri-plugin-upload" +exclude = [ + "/banner.png", + "/guest-js", + "/package.json", + "/rollup.config.js", + "/test", + "/tsconfig.json", +] + +[package.metadata.platforms.support] +windows = { level = "full", notes = "" } +linux = { level = "full", notes = "" } +macos = { level = "full", notes = "" } +android = { level = "full", notes = "" } +ios = { level = "full", notes = "" } + +[build-dependencies] +tauri-plugin = { workspace = true, features = ["build"] } + +[dependencies] +serde = { workspace = true } +serde_json = { workspace = true } +tauri = { workspace = true } +log = { workspace = true } +thiserror = { workspace = true } +tokio = { version = "1", features = ["fs"] } +tokio-util = { version = "0.7", features = ["codec"] } +reqwest = { version = "0.12", default-features = false, features = [ + "macos-system-configuration", + "json", + "stream", +] } +futures-util = "0.3" +read-progress-stream = "1.0.0" + +[features] +default = ["rustls-tls"] +native-tls = ["reqwest/native-tls"] +native-tls-vendored = ["reqwest/native-tls-vendored"] +rustls-tls = ["reqwest/rustls-tls"] + +[dev-dependencies] +mockito = "1.6.1" +tokio = { version = "1", features = ["macros"] } diff --git a/plugins/upload/LICENSE.spdx b/plugins/upload/LICENSE.spdx new file mode 100644 index 0000000..cdd0df5 --- /dev/null +++ b/plugins/upload/LICENSE.spdx @@ -0,0 +1,20 @@ +SPDXVersion: SPDX-2.1 +DataLicense: CC0-1.0 +PackageName: tauri +DataFormat: SPDXRef-1 +PackageSupplier: Organization: The Tauri Programme in the Commons Conservancy +PackageHomePage: https://tauri.app +PackageLicenseDeclared: Apache-2.0 +PackageLicenseDeclared: MIT +PackageCopyrightText: 2019-2022, The Tauri Programme in the Commons Conservancy +PackageSummary: Tauri is a rust project that enables developers to make secure +and small desktop applications using a web frontend. + +PackageComment: The package includes the following libraries; see +Relationship information. + +Created: 2019-05-20T09:00:00Z +PackageDownloadLocation: git://github.com/tauri-apps/tauri +PackageDownloadLocation: git+https://github.com/tauri-apps/tauri.git +PackageDownloadLocation: git+ssh://github.com/tauri-apps/tauri.git +Creator: Person: Daniel Thompson-Yvetot \ No newline at end of file diff --git a/plugins/upload/LICENSE_APACHE-2.0 b/plugins/upload/LICENSE_APACHE-2.0 new file mode 100644 index 0000000..4947287 --- /dev/null +++ b/plugins/upload/LICENSE_APACHE-2.0 @@ -0,0 +1,177 @@ + + 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 \ No newline at end of file diff --git a/plugins/upload/LICENSE_MIT b/plugins/upload/LICENSE_MIT new file mode 100644 index 0000000..4d75472 --- /dev/null +++ b/plugins/upload/LICENSE_MIT @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2017 - Present Tauri Apps 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. \ No newline at end of file diff --git a/plugins/upload/README.md b/plugins/upload/README.md new file mode 100644 index 0000000..dc3699a --- /dev/null +++ b/plugins/upload/README.md @@ -0,0 +1,121 @@ +![plugin-upload](https://github.com/tauri-apps/plugins-workspace/raw/v2/plugins/upload/banner.png) + +Upload files from disk to a remote server over HTTP. +Download files from a remote HTTP server to disk. + +| Platform | Supported | +| -------- | --------- | +| Linux | ✓ | +| Windows | ✓ | +| macOS | ✓ | +| Android | ✓ | +| iOS | ✓ | + +## Install + +_This plugin requires a Rust version of at least **1.77.2**_ + +There are three general methods of installation that we can recommend. + +1. Use crates.io and npm (easiest, and requires you to trust that our publishing pipeline worked) +2. Pull sources directly from Github using git tags / revision hashes (most secure) +3. Git submodule install this repo in your tauri project and then use file protocol to ingest the source (most secure, but inconvenient to use) + +Install the Core plugin by adding the following to your `Cargo.toml` file: + +`src-tauri/Cargo.toml` + +```toml +[dependencies] +tauri-plugin-upload = "2.0.0" +# alternatively with Git: +tauri-plugin-upload = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v2" } +``` + +You can install the JavaScript Guest bindings using your preferred JavaScript package manager: + +```sh +pnpm add @tauri-apps/plugin-upload +# or +npm add @tauri-apps/plugin-upload +# or +yarn add @tauri-apps/plugin-upload +``` + +## Usage + +First you need to register the core plugin with Tauri: + +`src-tauri/src/lib.rs` + +```rust +fn main() { + tauri::Builder::default() + .plugin(tauri_plugin_upload::init()) + .run(tauri::generate_context!()) + .expect("error while running tauri application"); +} +``` + +Afterwards all the plugin's APIs are available through the JavaScript guest bindings: + +```javascript +import { upload, HttpMethod } from '@tauri-apps/plugin-upload' + +// Upload with default POST method +upload( + 'https://example.com/file-upload', + './path/to/my/file.txt', + ({ progressTotal, total }) => + console.log(`Uploaded ${progressTotal} of ${total} bytes`), // a callback that will be called with the upload progress + { 'Content-Type': 'text/plain' } // optional headers to send with the request +) + +// Upload with specific HTTP method +upload( + 'https://example.com/file-upload', + './path/to/my/file.txt', + ({ progressTotal, total }) => + console.log(`Uploaded ${progressTotal} of ${total} bytes`), + { 'Content-Type': 'text/plain' }, + HttpMethod.Put // Use HttpMethod enum - supports POST, PUT, PATCH +) +``` + +```javascript +import { download } from '@tauri-apps/plugin-upload' + +download( + 'https://example.com/file-download-link', + './path/to/save/my/file.txt', + ({ progressTotal, total }) => + console.log(`Downloaded ${progressTotal} of ${total} bytes`), // a callback that will be called with the download progress + { 'Content-Type': 'text/plain' } // optional headers to send with the request +) +``` + +## Contributing + +PRs accepted. Please make sure to read the Contributing Guide before making a pull request. + +## Partners + + + + + + + +
+ + CrabNebula + +
+ +For the complete list of sponsors please visit our [website](https://tauri.app#sponsors) and [Open Collective](https://opencollective.com/tauri). + +## License + +Code: (c) 2015 - Present - The Tauri Programme within The Commons Conservancy. + +MIT or MIT/Apache 2.0 where applicable. diff --git a/plugins/upload/SECURITY.md b/plugins/upload/SECURITY.md new file mode 100644 index 0000000..4f09bba --- /dev/null +++ b/plugins/upload/SECURITY.md @@ -0,0 +1,23 @@ +# Security Policy + +**Do not report security vulnerabilities through public GitHub issues.** + +**Please use the [Private Vulnerability Disclosure](https://docs.github.com/en/code-security/security-advisories/guidance-on-reporting-and-writing-information-about-vulnerabilities/privately-reporting-a-security-vulnerability#privately-reporting-a-security-vulnerability) feature of GitHub.** + +Include as much of the following information: + +- Type of issue (e.g. improper input parsing, privilege escalation, etc.) +- The location of the affected source code (tag/branch/commit or direct URL) +- Any special configuration required to reproduce the issue +- The distribution affected or used to help us with reproduction of the issue +- Step-by-step instructions to reproduce the issue +- Ideally a reproduction repository +- Impact of the issue, including how an attacker might exploit the issue + +We prefer to receive reports in English. + +## Contact + +Please disclose a vulnerability or security relevant issue here: [https://github.com/tauri-apps/plugins-workspace/security/advisories/new](https://github.com/tauri-apps/plugins-workspace/security/advisories/new). + +Alternatively, you can also contact us by email via [security@tauri.app](mailto:security@tauri.app). diff --git a/plugins/upload/api-iife.js b/plugins/upload/api-iife.js new file mode 100644 index 0000000..5c3cd89 --- /dev/null +++ b/plugins/upload/api-iife.js @@ -0,0 +1 @@ +if("__TAURI__"in window){var __TAURI_PLUGIN_UPLOAD__=function(o,n){"use strict";var t;return o.HttpMethod=void 0,(t=o.HttpMethod||(o.HttpMethod={})).Post="POST",t.Put="PUT",t.Patch="PATCH",o.download=async function(o,t,e,a,d){const i=new Uint32Array(1);window.crypto.getRandomValues(i);const r=i[0],_=new n.Channel;e&&(_.onmessage=e),await n.invoke("plugin:upload|download",{id:r,url:o,filePath:t,headers:a??{},onProgress:_,body:d})},o.upload=async function(t,e,a,d,i){const r=new Uint32Array(1);window.crypto.getRandomValues(r);const _=r[0],s=new n.Channel;return a&&(s.onmessage=a),await n.invoke("plugin:upload|upload",{id:_,url:t,filePath:e,headers:d??{},method:i??o.HttpMethod.Post,onProgress:s})},o}({},window.__TAURI__.core);Object.defineProperty(window.__TAURI__,"upload",{value:__TAURI_PLUGIN_UPLOAD__})} diff --git a/plugins/upload/banner.png b/plugins/upload/banner.png new file mode 100644 index 0000000000000000000000000000000000000000..973b8745ac372df591ed426768500d48e2b6ca2a GIT binary patch literal 41691 zcmX_o2UJtr^Ysm(h%^NekR}KLsUb*HL`6|b=)DAj5D@7gC_NP6DM*nTK)^yrdXXMQ z=}o#+rAim+{X6mZt$)^f>n(Y5GI!3*o;`c!z86{=59rQbItxJ%or>~39SAzp0YR|h zGnC*b^?5Et@Goi?Wn*^;qG2Na1A~%NS-}rs?m7}_N9|O zkS~S!^|SP*>tzbIzpyLc&RTO6|M+_c(R_Yrg-Jl?ocp;5K9MA^&y_)LNbX8n_br=_ zo2uWhy&5?G{M=ig-GH{I`)d#K^9=NF_L%gvtW&A!4^HHnR19r;O`RUrIN8~(Y%TV4 zS8x2X+TUuPxiVacISi{Bck7tmOg}c>EbD<8c(!mpz19-NJ+*((m#NmZv*QUTKAG?w zbL@J-B#=Dj+*mK?xiUg^a?n#YTX^Iq#N@gBu}ZAj zuU+bNt|Kw!>7-XS44Z^9cRxAW|F%%A5is*g<`VcprT5x+^|yuf`d#zQ`4ngAC)3VL zQNu+Rrzz~irKdfwG^c7d_SW~GH3c}^EDhNpoM_GYFKm{u3p53|b%<8YZbprduxbpS z&NrvaKo4B!I|(U4C>af85DH>SNcj{ipeaUak>IsE@swpD$d?@J%^1|_bvZW6Yj%@k z7nYP|wmbam+3-Hpln`wd)AkhmY3X>7H%`Ov!%33-*X`wD=L4#2jW;IbwoXSFMak1E z={@|nC4Urm_T)7mb+9|{+^XK#=;vp>at^-5FRUslYi=Dqep`;X71!2_k{iu>NlWyd zl}&$1Hm7izxp^E8p$&5p7v)Y)kEc%k;6xXKZ`el8uHmqeePa0d>{SKK$xmBY@~Jm? zw4>9%u6e%*D$j9F;|0D4&0ztqGq2Ff^H=DI$<{7)Tu2ouw%}Q)ztkc>vp}e8<5uuI zB^H{0Pv2qfBECQqPYGoS?83JBB3-%hzw+~m`6BQpgovapVGBJ!Z80CNo2_x&^{VG| z`IYO&>0z_Sp8!Y1!!1jK(B)0`)k;-pIhtmz+(O9h`_2;cib zMyNuMMK<8%XnHeMMlTVTl)y2(yhNVY4n^GcTe;{KV#(yh9)DeSbIRE_t+zAa@%=}7 zmV^*%C-i-J@0sTT1zwF?48-QyuWw)*Ze%^yb_UTkzbRRZLaa+4USs%}02%3bGvsxE zTfyifW)#R`pe92q+?UxKw5FU15^L9mxNH5756VCUc>lE>v}yP|Wa@mZF3I@*OvgB@ z^+D?^2Cu+?nFLcOJ1F!rGj1vNsfjo>Fs@&Shfu&JB}h+nB3Ydkar|N0`fVO9Q8q^h zHlw{>WLfAK=b6_v&T4h4X+_;ddtF2~%=!`@Cg(oiN!7ld)dxW{uU~YQo3_6f6=qds zW{Z6Ttz=^`h=q#_4EPPTieg?Snd(~*cCQ>8wDBkmr|JnciHc-VX-=%Rp#1pFX5D` zQ1cb0B)gcH2R8&`vFOM}7IAYk-wScDpBan^xlIz=^>AqBJ$U$Mc1`4@hi|sd7O0L| z9xggfWFH=C{7&1673%mf*Y}EL@dtale+`T^BekF>?Pl$!2c?RbMZ-9R^!3qytr8wV z>&?DLnL$HsmT*Wph=ZEd^x5#P{h`#&AnHdf1ngrd0d6mJp0^W4+E@;?58H_JMe7avbzqx3&g7(anti^?&D<^mH4X z%|EJQAD)boVlUlkN6GmA=m(JQ^A`l=j}dXY;yl{ORCzDAh2;Xi8t?7L0CV{GVy!&5 zwuM3-`uO;`u3U_UVc!5R*FA!>+EFBhxt}NgnS1sT+}Ec;ruB;rUy5<^Rb2h`o~tHJ z1*r`nA5J~298S3>N9aL5B#So;UrN|F9?LxS1)$|y0Au~cB%m=~{#r2hBc(DFni+B_ z@ws*Bf$kU$B+dUhNglO$NzOL2h7D@U0^hMmiiz)q^iJt-vM-oagVP)9Ttyp(D$(}# zO;2W64o6;D{AnED)6=44txHW^)>&lZXlvJkLV1L5ikp2ZW^Hw%!l9DEo{Y~8=k1S@pmN`@QZA|W$^oLkvTQ+ zO7m79L~9m=x|(RN(JLosOOey(45b(8~%|6zMt*Q%Di#t#u??=5da~jIY~VN7XMtcN5|Z42#UWSlgzTLpH0bs`LxV zS)rD`!HB8Ys(p`i7oj zvl5FVGH8*~tIYT0c!uZtU`A2LiYOs}81jBDZ!FTkdZG1pR2~J?6fN<#Mk#G69-2Xj zz-M#-aA9!-1-Rs!FdR7!U()ts?`VHBoH;E97NeFZCIuB}Qi~1+4?K^ig><8f(cSKH z3v-h*D?iG*hK3B_M4QZ;<}ngH`Tz@r?+{Sk++R>ghs=Y=mgzVdKMI@cbRB2fA*;AM zOV_KDGa{JpLCe}E+OyhItp5&t&Fq#aZedQoeMNuZuS*(yBsGLpGgY=^vmygTf5lie zKT}U-jAEMsh1RS`vgzTXDvzU?pN-7&|(X zoyT8o^ZKYz#s6uFKAfzWYcB1>$mFDXc`F=Sw^ZU!^U!inFaB4?>j(S2VG3JC$&pV` zyzVnu(^fKRBmH#5|M>ij7+!u5@#ccIg!5(AKg~okfSToYSy)coyzWu-C48IehtJLmEKI1fz|&GNCSK^`_-4=f3!&JfRW2ySVU7do~NV|M3$7(|IZ zT6*zRUkJOm{s?h58`z{{%*(Rev??&!>dvnJc28 zPVG?%W;x9)bsk`~;TB^YEGhJ0BrPLi1Hq~=m?8Tv>R6=6?NM0arVnuQcT=b8xe`k3 zJ(Qi#zoe2>Sth6Lt*5=ckYrl)`$dQ~iuI%OHOM{D-BPF^UivciwzIP{`vBE%?OM@Y zfXijiAf*4cqG-64VA-8BVUD=9PMmWNL-;XR~d*)PSs;Zn7 z!t{|I!l%rQCwZb5deSX?-{1qfGbwjrHHoU`+sCS$M&#I|a`!-2yDE*~E3$q7a%`P_ zk8Y*Fue;y~>@v)Yhg&fu_w1Qz)UlmW!NH+Ov3!(nh47&D=p91&>t~x?GAzNpfMpk( z*|q2m=rcgSLwD4m{)7y)AD!bgXJ`@qC$<||@bm`P`qRTM80Vucr9*yfgpPKWMo!kd zi1@qEgT%pg>Ne}%YOd(k4;G3q62&lXbrg8!fk8Ue3Tde3{V^l4ydu9n=~>t>UAmWr z7!bVQgDb7BYZ=WqGwk(%H2Lt=iA#y{uFfKaZ|Zo37V-o|c`Xb1kdl{C8Lf5y@C;;x z0p{^VCsTp1b7>@5Xu!RoUehEy5H5~M`5x03X@?UA$uX{X)clHp+a9{l%k(K%NSBY# zYVI8I*Bm9HWe_Kt*rzZJ`Wl&v7zi?`D&W-RRHgX&v)qC#iHcbW>$_65#kZQoWxCB> zbRj{dIvG%#TR)(Pdx$5x+ZoD2QiJr6Y$~a5ss=crK3K!cz##-Z-x(8?=>Uu^SmzqwdADnVm8-+CxCGB z>QV&Hj!X_iU3J9jDuj2!@jGCcB^PGAFQ3OB0%j zChX0pYvbz7lh6pp$3gj!A8(h>;zf88=K9DmA&@Jzp~P`D#r=o-2Y$O05{bMIN0Mdg zD+7bQl@O3mhwHUiSGk0t#gjfpMYS7{QF^LRM~0=(Gz)%WE-h=kXI3`(kQK_xr|-z& zrJTtqrCt74W1Ia4aXY37AM$9Cmu$OJ_lem5?g4kFSC7 z+bXxE0tvheOY*?TS`W-cZR#U9+Awn8gW|p=8!MG1+YqjC2~``413a>sFPLYW+_`=E z5-n`Rf|6NB1rgZf?ZP8-m73GW#rV%TNZJZevGZo4Zd1ApLS%p3*;w-OUFP<#ZZRMo zJ#M3{jGK$G97}$~W?X&;#vTNW+V+~h@qK+Z>;*R+M~2V=cst~)6(k|&nU##j6iNr5 zrEaYQ&}c9N5uw>Sv+g#z0D)N(Dl^!#01vl|&;=8Bc<0@lS((=J-<~^6~NY)9cytit(@o-i25b%B_U*S4Ww?K-a=A zTh>a+K>dcAHKtn>GZ_?2%boyD9z4w#E4wD*J~MSfN-H~vvQX@?oSlKke?Yw=YpzR&DSC0P7I37W+)mXP64sPW z!NeY&(djI57@lcK_+WntAE>5<2>z-h)Gk{>CSsV^RxU84qcjgv^`B}1(Ha`G4~Z(F zrCTOJwxnSHBd*)wd%*h@OG^?wa6sD z0r|)2u@V^)M}1}}oDB2~4ggGSmhw>4>d7u)uDwAo=x$6neH$ng*(?Hg)2>p^1^EMK zU;KCv8mUc214a!5wf-1)U&MdmA+cDj0u6R zdB9->KSD^b5Pq=y2Jn-VS5Z>$0SyK)cB{OJ>~s(;E5I~v3QMCS=WBNGUeUWdx6!u z>XeXRUu16Tp`=$d5qz?IEgUc%?{pxk;Sc1-w^Z z5Os4defz9Q_d`wT-~Ds3Z?t2gl+86OW3sf5JJg;7w167^9$g`Qj?4i@hU9N*#fmW!P! zA#mf7p-LFt_2V+%P7NG^ltHg!ZN8wC@(^{K-8bln<8gPU^+xjyLl-_JRjN@^KzNfI z$k_NQ+nzK>sQDdd+zWCXIuGhKzc)V)&3vB>uxtv!;eWMfD96Ow03hmAM^x#kXNifM znuz2e;Lr*pmePY}3=~$s^K}%gHMl#3jzn=&K8UD@d104gu{mzs zaYL@#Nm>`FbFR_e))p5Wo1ApQyS8p)Fj5$!Q1#lm1k;P(tlQs&wWEla`Pj^>EZT1u zs#sz*5{1Im9MgPdriQcsk5lK^wRJg}PRR@ym>Byw20$TAsuOwF|lwbPzc z8-+s;Vu@Jpjs(#7!Ij|6iu(wR-f8Bojx3U3^SCU>A?Wzp@9e$ZOFN;=s1| zdFCpdC^=nZj62Ut4%Hx#a9XA2ARwR!2*Qb~jB&YR2`LU?8?6obrL^#hM@=N^8LXrI z*6g>9+X(Rt(nD?@hy7%Jp2@J#%(&c~4En8>J54w&K9>X=61_tJ_i4yV1L{CyRajx!w>~RF%KyIx^Ni zc03BUr2^!Gre2V0KvoJ}8Y!_lO*du7)3p(mjlZ$xW@Y&DB5`u_gDv%~*7`#2$^&p{ zd4Rr?1exJcg=+W)5t{a#LTaMF+a&S_{fvItL?;ZzcU9;;9aFIc+vTe|`aEzxNVmLT zRR{65)Vq&}go>|HcUqV7?AlbDt6&jK=S@vb$s})QyE$BKN#-3aP<3H|Jawb8aa^CR zZ$zkZ#$EKZ!fF7LDxDFxFNR{JK`OCbXqbt-q2OYbK9dIeh+;dof=&+=yD-y2%+jG0 z{=M?KBKmf|h{Tyl@L%&~U8lcxF9e~GV$~tOJ{C(Xuf9z=e?_PR zvLAM8j4P%k##vvMg?|AQs(P`f-$^X9xk3tpLbK4=D!AnKPXzIf^;>9~I>2l3Q#Oll_Q9jpIzdyzgVO!g!)CeJh!&14lu;RSZ zXBmY;L-h@AEO|m;A}EK z(`a?pRUU$y-+Lj!;`ES}e+r(^^y;c3vouUL&imcX1tzT7(zi{a4&*(OC2{&n&v(>_ z0|QK|0E#0$AR=p~pEaVgBPh(b(+_jPfhCYzgYXql(!un*!tqV5`yc2n@>F3nuMZ!9 z>T%tY4J*2|qlU}mC0>{vNkC%j`}kX`^+*LLKZ}=2D$RV7ABDN*2BWHro-nkXn`@7JUNo>qWx+?MY3FE zfL!$CS_Z=VC%qQlP_icOkm<7WIws-GE5^hz4ZhVd*4j80XHEL667&$-m;emF)!O_+3=SWLmz(!~R&`&-BH?M9`VSW44grpq& zOWy`W@QQCgakPRIMr7mh&?TQ^;cOqf!yxz0`VHIK!7>ZsquK{%`IX5 zkkv!zNiKbdYzuL(+!UEB->Lyx51Sk0{d-(`7CmWZ8#38;DKWclnF%}QT{e-cV?-M zmU>GY@!g5Pxy-GPLIEQ7;J(NYKgdPtR^i&y8%eblsZlMUDn;-vy|R1HSROk?nIXRI z)v{07oLeXo`#1!t2kglPp%^n50$J&fo+$^me z6NI9DQ%u`S@I1^60U91(tei0&MqtWa#9bav>fIaTe7^Q2EJof~bHPx+s|u3Q%}+~R zi1(l{5&QT8Qe)NCb2#qbDST|> z1&0ie8_6S-3^Wjex9M|VErp@NFJGJ}-Xc+@f*Q7_V8vh3fWk;u4Fy{jTC8g-x` zcxbGMLZG0L@)`|5*7Y7HM@0$s!%MSl+^k<4ZoBrUKT4aP|0 zxu{r`BXrzsj8A8Xon8(8ecRGx-$6mbvG5_mj~1`qA4HwSbLd8Gvv5iGxnT2cBTCug z)Rm>Sd9%72JVmJ3ptWl5Y3pofrdpy|IUI5@0ugoCSt|J6w@`|SLjxh~#xUSi9mm{Y zjQpu^2LBdR|E#wSYvtUM7lCDg0M_gX{Y9%3cYw$61Sp`%{&hqLPbVXL2}iI|-|CGi~2t={Q# z+$J+pW1F?EnAVC%1kRz1coR;mWfDV9tM63q-uZY}zWcFi@K*>7vR}Lo0i$yCeo|O3 z?YPLHk`QW;htSHWBqaUA5Ms@%VSIA&lbr2mS$;v?sHXh0?M5I-goX1DUh3IEtnUDc zM&~?Yr-G1NnNqrizee8@3KTUSAsAwXI`u~l;wo_!K#86*`c6aa9dtAm>3qKN*f74t zD8A%e0?gobU?Q!?MS~64-FTW!01o=`Kq7=@^c5uux8;0xRy6Fh7&ESl+F->{B}uD| zY1oJr;p%mXA7_ZH0C+R=E`w+E$jw7VyeOtEeogx&?!^LXj|_ROqdh9|1v3OSiwa)= z3izi_^Bp%IQ+LfCh{tO^Cr_%GP(&G5rx24$E^%|8F0Jf+4HwYN+$|FkEUT!%>5xNMgeOf`-165W z%o+~EjiRJ(D$#gqOLhr+hA~bK4C|@Hz5of*8e)U+ocz~fsR@j6 zFU%ffDO!@9t&Y@FiuVxym{6*oa8|jZ6qD=#VP7tNbEXavxFZc!a}m-OBn9>%XqX z`I)7hq^H-fKFJgA$N?RVfq2bRx(-%gD0g&VN@*#efV6^S4knRmprH{K*L{5iMsjoz zv?8!u#c%xvG`*5@qp09Iui2>_>AQ(L`Mj~w=r#Ml#f&WY+zz=C!C?%TWPkoT0#1}^ zBdTNcn;{+~c}EaRPJwz82Dz}-Ry@kf@3Jng(%W~j=)40i^N)Vzd+%*u^?ZJ4KBrLe z?K|!W)3rAtIMy4MY(EizD*xD*4-Ra;5?s3E!ObS+8!u#eq=g#NEyw86ABLNrP?%!g~mY zEB$TFa8{Ju4$-jdkE1~F%UVfY%crzf?QMT-3e6Yj{iTUN1oNH!d&AWc@gVD>VQ*@V zbtqlU3dmCUfHg#fwf-#F;bBQnj7Ag2+v?rHIp^+CK38*TR&DnZ6ir~v8Wp|Fgd1z! zd+7+rR||g>qZvE^M|!^hg8JW)`tCTz>pX0+--!rOns&2lHzh9v2pY;Gw{#OV36jK7 z;HcH(iX?BF<%JpfqC~63udaLCyxNPn42L=@deRX?mg8=IW>FJk*=)if30xKP^V|6Z zt*{Iuad8rwT3P0A3Gwv?sUSM8QY*wst5=7K=_@Vu&Fq(<&d2#IF{Y_xUi$pP_3CPT zyN{VBc{z0}L_8@qs?r+dYAym7=z+B27lDsoh`Z`H3X{G=^4weD-7c@$nhN^vxaE<) z#~J19t&rO>)<{)0i#%;7k1O^Co;-ux8p29di?w@IBD<+(3@yYLMp>IxjVNsjx$46I z%`a0flUz8$C#Y>Ar9|C5YjyLg9u-n` zbFoEZMIGj&`reEY653SIv9RMgn$;o8OH0v>y>eDhm#0ovr=%hGKff3PKEXmsmkl`? z7~Z(LRH;aC5DJW`)Ompzy_3ufi(zE63rI`HkV)10`JY?8t!7-HtB}xGbXrGlP0tu& zC#?Up{lRBeXe3o+MAS{eAdsxN$VtGou-dXyF98M(fxZiji*%_TsUBttiiRy2-0Kkf zKG5FCUaKRT9rAFgXJIJvEEHN$DD?h-9DSFwv&uq};tS|jN6Ujzu0Wvxx(pb!JuUhS^xZ6PSZ2=06zpnBw={oTn{Xs8s0d_(|yfUo|-EV5` zTuuNl7t4b|8F%^rYZ&(M#Ne_fPI4wtxhDYz6&9x1JMSmXo`qPeLU1obd@7VeHu6my zLtXu{0Fi=zVxiW7U>=)3h6iFr{-&!)oK zvrq&{fB>J)rp5m3rwTpR2c?xU#62LHK3s6u$0eP$1Tv=#sbqocWU%Yk(af=^_pX3t zy)6{ELf)cBTDGh=x)@gt)W-jvgw_f|0cMLBu)Gw>eXBvTJ79HGDjt_aOFGFNgqw5y zcETXvzXP>BXtG3-)YuUKc=Av|C^TRYP;YJlmmC--Q3atua{@m7v29!4@>(8tNs%@6 z$uG74_qPb-I2~;Sj*W)oa0p7OBuR=3n|g8Deq>oZRwqCZ&d)3(_f328gjC3vBm#gZ zE&=<0wz}OlE8=RS+lUu7yhQq&*wOMwfs1*&>!6>a%(@3|K=w7DP=i;letWk6caF4# z3QQBpP-D+h%mfcG(-O75G6j}>m_n`GNMC)|^kUo(683`+elfp=QrGc1XwL1lq+aGS zfv}*T1G`Y%zaRK6_GK!4;rQQqpj*?&F($yWWkBi;V<*9#M)(EIK8+{ceL{sT*b zQebz}cJq6be>v>8sn}~k`O7vvf>61FR&!@aM|9r3*w4(yqUS}Hb>)sMxE&ySuo6I6 z*6z>CH2yehOFwFx-mE0IQP{2SkxD;av1LhzKN0m){kP!-HZA@gqK4>O&SL`iX#huq zJu<7K^s6;EAD=^Sc!Z)N%Hnv*Ffr76lre7q&D8!IrAMK=etS*aPM{x9|Fgfj@U%TY zYfdK!MOq{McReJdCP>0ukK_w~up2~~6zRUuy-GgK7|QgY)yqgJqTtbY?qluE+Q!3L zVG7*zE1AhhC##LeRpF-$88oXIXtM@iV^aP?kPY8VFux$vdflc59Ib{iw(;?)4ZI4b z4d<1A@-yS;ubfUjZPAL)1IuCHga1bEjqw|mW<`SG*|`|=C>*U#0a?|7XI8d_j{FMNPOSp7`Vjw!G} zD^4a^$ZzQlvVFbb&4|zSe#D`*iTG(e*h}Oi2Tlo6}y*usfF(h z`;^fU*Mo@%rCldO&c{PP$*$~AtkteJC@E)!?+X7tNj&~_a=1RVzgf!AxHlah{-WZo z;{wL1O3vx#?znsSaW%~YKPN8*uZc}xX(?!MaJjb4>dOdv&dAI^iX%JpiOAaDqp6A# zv%7N~G0VN=(vj^Ymenhg4Dx?&yZ7AO6^W$4I26^sSx~zzJzVB>D@);5%<#{R7H*5W z2F#=yj=kG{ucFA$vA4)FaUrQN?zl0ey3|dorQRWt3ngg&ENLTg zAOi8nlk3Okn*srU1=I~sNb%QYd+YFrO<~qR=;E(1ZgX0fNNwY`d?GDlXlI2nkoHJ^Ge0A`SO#$F1kf3J5ehFm^M!zkb6$PyhY^JTwb#=uPEnArx04G zxS#0ZF-Pln%gYw0-tKysn^kq)KPY7+j0fhHZ#`*W`_cX4B#(xaK`Tr;733p*>4)b5U`)@XNMMQ$&&dgYX%xmV`! za?Z+o28F$!*s2AE!jyxAVN^?<8$<3A59Dw^Pd)Wkfd7Vk3>)MZo*dkqjPefN~qK>lp)z&GgueSUoF zZkv6B6Na1Rz`HT)L&s{?$k&ojsq3{TX%8l34rNb^bj|xysg~?>JQ}O~I5x{(`Tvbg zcbFF%^Avh>v~6*^JzZC`_>%ia&tLh{SGaUSpX-f7jj3Vn9|G=0lK1>lt%ly3h2Pp9 z&KLN7UHKYe9r?mGmAHU!i4%m9RBUfnH_B54kMAp>^S{)~~ZK{&GiTVRO z`Wd2w%w~6AsUZ%+F1@zvkW=V!Ul|*|p62Q( z>&eBE~qv-4`b-jR^bN_+dRC7?wG=_$O-} zo84(_MrYjdXYQlV8Tn5gP{ps)MUiD*xy5q|j1YYxn4$k8w|2mO(vCnUP}QLKuiGqz z;ZbW~50v{%yK;Dup(6d%$xfB+`M9en<9tr#r=_b&N0bKfR>f`8fVAGr7F&4FW7szK zWG)31p(4x8@dsqY&x2M5x!1s2KH88Itj)iYtFC}u-vBEpGE>KEPgBI|H=5bi6B|yC z*JZ${+&L1Yi*DD0TL<6gK@2-!nNUAY4wj2#ZXNw%V5b+G(_IdqDmYsWmaLl&X#I3@ zH3=VD@UF?T=S>E3Y9SDD6aQ6N!+>o=me_#1J8#x~Z8v_m z2kbGNzTF=_J+P1|rq0bmmVe%pXuWrT!d{l2d19q-YJ6-VsbJM$)S_;yPc1TxsG8=t z^J5->?kB7JJv!fPofZy`8su7TZG2_w(xQF)TDh>1X~cCxl2(p#Ug2-{tz$bda~E4p zRMlTXr{!OR^=DErxo6QaJ^48ta6C-K9kT75nt@QSL@hhq`W{%_hOJC-f`f&&lN*TW zDiS>fjc8Ye{qUtB;kl{({U!GriSjlT?1)?ECvgAl2Qhglcj>s>4&C8*bJt=QrW&TumGe)q(we8^-lGly_mF+JOC1UUiic1%3huQky#K z>p`f_?;q))qfRyP`X@Qi2zV>N6Y;1_)$2`cMXuOpcECY)7ue!pPf%OV1R7n68A}5I z;Q0BX#uM+&VD6)g{WeYi(!IL+0Nbg0mtTtRB#bc_ae9YR5nA;<*-UqqtwG7{Zx;pB z!-kf_ZXH=}_NSke%SE7!Yu5mJkCX1W@a^^T7q{yqCi=HWg@ zp(5(0=wqqj`KX&aMsVmTH4lXp_n<)>-f-s=ffFNXCvAwwIjA48pBD>^)_f|=Ddi;p z935@;$oTdGKz2@k))DfYnJ1;BFc0MwRVFTQl+adIZ~ai_6mcpI3zu(1Un+2aCw}N$ zsU2G}(JHi*|wF7Xqocj7&3MI=J`9Z+51& z9IB@+s8O`4Q^^2M0Yl5mm~$Iltdap)U^|U(R2+r z>{Q%UH?n?rIp4l)-Sd2^X}ss-iGy~-hCXN}ZpKCNoE&}jlG+rb(T5w$IhPYReJZ0; zix|8MEq1*W2657J4To#?p^x&0<$qi(Q|H`@nZc~FGgW&1`An5!arDi?OpgOiw8xZx zfaB9i_^(8R05W=pEunn9wtTx+wSeJ{lf(aBg>iDJs;s;u{a)|vZ`~j}g7xo*IERJ@ z4Z}4C!9}&WZv!|}rrSXqYTpujeCH+A(^WHRBE^buQ}Jk>`kZk0y;>3da~Q#9i7gYi)b(&CSJ1shE3-`k^Dp`oj4jNMZH1L zS1?4m7|165em(w>u%e3mXuqGfe5TV{$Nter#%8hAq`EAmf9_E4)8}l%S`)u(gUrat zqZAGvWsCZ{rLEV-FCv8Mo^bYh9d8e(6pnigV}eeEO>A@2;zc1X@aRBsf0hog-9F|; znu5>g(?0QIou9$G$rhL9V}+EV~-eR z*UD-X5$kd_3h;As!@JDM2fL>yQ@h@;((*W$hj1Ph3g(AzEC0Kc=Mhq+FGr4DLq!@H zngFL~6&3PT)YnfC&48j?EbT|{mi*hx!pUJdWvoEKGq~2+uj+E^%dv*-n=uzz#LOQ3 zHmJNIoN0N<>@J<@6m#F-lU)z4?uv)$qPoKv7;61);@DQWc*CbN`5wmKZJA*2O4P7f zJ*t^M?pHh{!**+=D@<_Q*g1Lf7Nisdm=cWak1O<~{`+b+hc&m1YERG`NxLWNUj}Q) zv-1X8Hk${jmsQn58}5pH-@RvYreh0qBzvK4+)4OCfn|FkA5KjA+mX6mJG;^BqX>n3 zmI;tUT{ZG2do${ZXE80AD7~$lG52qw z_BZFm1FB)o;%{dZ&J1u)TK zQPC$T>a!(<)#QO8Stp5}+7B)~Cd#QP5y)*`3L6%RIJx|&Ye4+RsIY85{X`~Ltp5Rj zif>P;Tgn+Si=xf@~HxbmojKfiwGD;Zhn zC-1yI-u+C)ZAOl-Ni|z7Q(|BD0Rff*qHlPa_R7%kiT#(8-v`Oo>%x~S(3M5pM?a~$ z;0*G;PaHAKm$|)vzs*ux)w&-9cK_@<{CEE}G3=Urx6S+8uyvly+?Ik5VtrYY*9n@( ze2{FnZ$?G$s9hXKJq4_+ApXUu)7*5WpOrmxYw?!Uo$}lSW$Ml|^{+H5oAi?Q4~82j zq24zkTnA@4J|3E#F69^PFebDu$8Kt+4>RnuVH9?8@z#Be`5i-{yHf#;#hpAK_zk#4 z5oNDzZn!m`C7O|nk6swg>@Y|g4HXauJ0{xH9Y@8PRRS%*?uMo8LF(;8P*?iEhR#Df zO#UHk{$0KsW5RO3@Xm!YH=T1~>cy3t9Y{TjrV zovKG{=Mo#5lKrZ%LOO#V)N&7+KW|rj++p*F<9Vt<=?D1D*4|(NM1L_D(7KGu8oWRE ze6+?I1aRE)_gz>XAINGEZ0cJePeu_q?i-Y(Bn7)JEniZ#uYbQN$y&SS?w3CH1-xqW zHcOm(diH229w(agav{y9aIvq6;nui|iyuYX!MFwttGuK-m@8!Ug#&Q$Hry1|zc14S zEtxv^ydLgKCgWJ9&PRw15szc#T}{HK*3iNSLd2uyzdWZ==ay!ExU{^mUfrZ`<`PW5;z;H&C-R9WK7Mzl*7QSOa8_OY~VqGYOtgq0T!# zurv~)7~@SHoSnyZf7v=Y;;*!|zi&M1Xf#`#vbntgG%lk!+LM#r#>PG0760{Jf9JVD z!~c`__H9pB(nS?){cE^d3+n;3?czuU@8+IQx>{LTX*SXwSINfl?Ra;pG0At`%|pI7 zU12PHd$2p?z4nH!l%3`fE?UGfVaI26(^)6;V?OrVl_>c>`Tf~gy8N;Li1_P69-7Ut z&T}=H;u-SF_OTims~(2ne*VXJVSgZ7VUVMlB%;kno&1iv&zZar^u_^>f%TKMbusg@ zTSrZ|%tCCF!pLz44u=w9#b^Wn@{SK@+o7=St$s}z(32HW3$T7NyUYJ`D6#P%zwt3^ zn!5gx3TsuA{Z50B=MzpopZ&SS-D4R!=aZjQw;pjl`x%%;_|516eop5v?LR$Tn-Uf3>QwQcve=Keu=8Jd>Yu&8 z3Z@C6b{%}01{OPZOZo3~5hWPJ;1WYkG<9+z!iWA!Y#?=QZd{o^fwrq;AIMFQ@Rn|L z-}DMhUTtMCq83#V5j*yu^#HArd}DqKXTbt=+Jm@_bd+&gbVQq55(7XkfDXl$b`RpD zH5yNr8z+5tYo{FN=ktLmI{lv5c@4)a9Zyrk@zopw@6a{JB(eVh&Eksh8+MaBY&CyK z5=Ouus({|H$Lsq1f}1~s8P0Y(+%}kP1Wf?qHx}zg7vHuS#_j)UQ8%(SvOYpzNX)~S z!geqN9d{U2Son4gP?&GKIubZ3KhCOyf{?RNN?ggegBqoZ$4QA}PPL8;?sw!q{V1Vi z4Y_qPbIY9Ec3v=$;OxJdLf0P+Yeuis{rN)EWnWZ3BxC%$x}%#P`d4n@ zSS;sO+I00ontjdds7H!A7b_;c(?0f4k+}@z~d|hj9YlZjz<~jb(pl>USsYf98ZrS64-1CoYhb zz;G*%#Fo(l8%>JvqJZONIv3g8K94w0m6Yp{12VCABCw;j~3HU zK)2MJiGEFjyBiky26TY)Q&D#i@xDvC=_d03T1)p>{QbO+M=j;XY)`lp<6i`~ zzUr|6L5rViq# z=aHkNq*Yz0Dw62gZglex5w4y{&(yG9!>rrwY$5@JF!`pnwhr2$_D1Q&fp&UL>Su5W z4vRh6zjeA$M%(cfWFV&{8@)rG@H=-NAMk%_0B@V}Zh#RI-ED(6w%cBbgU?WMi3UbM zX~Rld=kg*$6EK9Yz*|0aipWR0&Q@*Z9-PVYD<^+GI{Pgv>t~MYf%pRyvmW(mbovikQwF`GaWyOx29f=F&RVFJ*l^Vw)V zcC(fWq&<5VSfc7tAyyC6+%8LgYxj|iE)<3;5W?@bk396@;X-o%ICZXFtpxrtrrf&> z)TaAa?+~sU#J4Fq)Gu8TRwy*9?t{uRIdv16p2TEy>T2E`g!-ih?FHxGFSla?Hdq3_ zb0`fjO#c%jobS@_kP*+7x3K6JgznoJ#=Fwu|Cv2Ws+1$92(PTs>py|kA@lLz-)!GA z))S3UqdAj5YMIPfy8K>as#&Z|)a<6xD12R(~dVWd?{AV9~D@Ebi_- zM6}jA&Ct?Xa3=g9EwcanlKF=A&3}5mkJrimg20WoSoR+^*WT%K6a5b!w`>7?P00y< zp>p}QR%nUO@3&#eVjkz8w9V=!4&vNS>Pg)+7&R*^XQlD=>`jweZma^TMT=@4_#FTj z68TR{sXhQ0k9*GQqpCIk{QgK&|MoNR2$EKvVwm-9A^z?6H<2ck0UhxTL zPC^5u^M+xq3=$gnz$6^_tN2os9Pt`;9$q%e>)Sgvv!SSoobv(9rEyEuuH)j zEi?K5c>3~qsP_N=L6Jg5DP?IzcB8Tr+AzvC)@(6kFD_yXlDJW*v5kE%>)2)A$}X~v zU9wDJOj$-_4Bz*0Kfj;9?&EP!XU=(__wstZp3Ccb78RUuv%MowL7Ukdi%c_M@BR#> zFe?3TR{@dXd=PT$TjZ|@tKG*?-h89^(^VHS;UNM1%Fa~aK0PJWm-9L)zs%X}vv-Yn zcG;Vw?yzPxRPZi@k6AgBIW|03$4ZYI#xr_@;P6VC={O1HJaYmwF%aB>+Zx2g6uW2^-tzgVGP!{GDX(IW}xUhy{BE z{dbtE%NkgS4_L~=1nC-+vSr;_WgMnDDVR6EerEAuktC@H@{NH@GHJNY8>i+yhIREs zx|I&Lh}uSuZr@MZdD(3q6S7RrEF%$riY@AL6A$$o0%n8~(<*tCdF<06RKS*TU?nb# zGe#4|BMg6<(z2BIaDm870rOJPN@$UEHx7v!{Q0rKN_C&wFKI^FU61>KiobRtqkm7+tY4vVgW%qYYO3lmSkN{F80in-k7@3xL<=u%>QUa*iz+XA>=j6gO zQfKSjS~j8jg+-4SGT6x?_2mZI$D-8!=%X;k=Dy|hH(#DhS#6bGPy;ziPy^V)nXjDP zm?j5QQ_EFp7v80%{sH)D6}BjmvmBK5UFeq4uRE!JEpr zadGm@7su=I7?N|uD0@=A7Oe1IafT&kYvjv9eK!}v^s9q~aU7J_o{7pp|D6kag&KYG zU^@$j=l}{V6{+?ky#{$AT;MEmf&wV`$(MrEz~s06lMoJtOD-xq-=c~)-30mGWuNH#T#&G=AKSDsAUwnz7LW4igA<(`uL@s}?@lUMc2*=g2Qmk# zq{w^P6E_f%Nf*L=wVo6kmiGI3oW5}o1_+4_4HD$< zBJac6bCugag3K%asKHY?kaVqfiYyvlPFL-jki7a!8MTr+w&t8kl)@r051tmGf7H2) zT=~a6oRq)`^IOjQkeH!eGDjt*xpQRUF*egkksSr@<6f6!)Nm{U{v|d6J8bYA5u(D& zQcS8OM;$pgzk5ofHcDx|GxIdoIVY_SY?etX&)+hf#d{)!STWzh6WHtg@ z3CtP3NzumW+$u%~U&e}H}ms5O8Nvhw>*(LKW9!&N?#Sjj%*!Q-W;$tuU|Cz zGv(ROUR>5&8^wd6D~ZF;CaUyXHEn7)UB{L5+E{(yB@-p)+Cnr8<9)5Fh8h`hN%<2; zU}4f9xXH~k%kyexrL13DmO+wZtrI#Ckk|RykKf|B%Jyl?@xFn49=m~tsICuQO~rSt zM15l$$Q|JKbFTJ*TH81RX;m;moAB`|&$B&MXr4yIuigtQ()T)xsZvyMF7tW$BP1B?ffXo@0=~z2ZCZ|s(LEvIvaieVY`N=I_*9}`$4WG zhV;-C!)(AYr1G&}_p`~?xS+Gx&Xm9o&?}{jk`DWtBBVZdk2L!&Bt&<^LGb(ZDVWs* zS*5_aY3Hbz-H7%qwgPAP%b9adn5)89hn61_P4Ke7KEdo4wjr{gNli$qRZRQp zEWwx1NCRI&rcTMe(Dssy>m!$HHq$hk%aw`=FKfYAg`MK>9ti}Lg}7jc@b(!`C%srw zIo^3H5qb;>HJqRRBihvqHn>)41`cp!!+!Pxa|2a%FKRz) zJmlpA348sl{?}N})xQU|aQcdsJBMh$^u*73UiUloDWaKZhoFhC85S^bOek-UjKki7(2XRdQ2aE=BD(LKt;H zNmX(#63I;8NahE%xI3&njY9Sj4ynGmztn@(Fc0?@wz_U65QJ>9+d}L|Prtub>xYp~ z03u#;I3us+Ocvt$wr^q2_5*-5u&~H;I|M6ZD_4}bw=F7P_VrYa85d-E6u=jx~NZE7p@tq0EuM*et zZnOnAugecQA(b=eBG{V7SSBPF8G4hdTn0eq1xxtECZ|eUE$;*I@27Q<;WC9nzbH=>gnDExC32W25^KKQrGJ`j&=UE__IyHG}Q5by_}q;YNN za@{4vqSf-nol8VLw%e^UFs4Aqb1-32IZt!Nc*Zm_lbw^i|u@AYXK`4Tb}>67^$ zZU9*cx>ztNnd4DuBzg-ZUZJNl!=wSLEC?rezl%~>^2p&!P-p)+vvIn_1W#qttU_Q1 zVtw)1K7CtO z+;Zp~H0{&3E=FZ}1~6E60=)|PFwpy5avj(D7m9z)m4ey;)h!4oj5Gt(qD?R4^|Sed zei_=-0Nm5_Ke9(_QCk=SUXmq#`(jiEbN3#d@#EU|Ubkmr7xnhBuTL_jA2}Hp7z1@l9T$`5V}LVA`&knJv*u z09VryKx8%qUM3J$dRlU7djWyN%DLmZuBR&Z{*wEiJ(KNw*1xOOD^4BPZ-;>}4pU*> zy>b8NQ|#cmf+*5OLcU7i@N~{`S84% zp?e9>j}RU~S(oJLGQ(Gj^)6Y(cNos*huCq(K_aH`3Tu&r@`)TEQpjaOem4vW`Gc9L z2ONrBo+og(dp>vZq6WJ~e$L_M8&w3y9hOGJpYU}&{@IhP@o(PfvxF1sR>Z7PoY@6!@JRLnb_mXHqRQ^);<66CQrXITc%!)jRp#(O zRB~uNi##ifeUi4nOLVBtyn*GKkYqQecr`?ZIlFbd0WsG=e&Aj4xqoY3X>dJNbSe3- zLK;Ve$IRasL2W&YY=BCnM)4#yv??qV5_!owwitr)Xo1{eK7b-~>{8tKDVL-?0kUt; zv5pqJvT=}4Nnv=#`m+~#1;5F3l^4eeph^NH<_=-G+GL5|s|5DGgX<>h)GkBOXS?fr+OZMCD{=Hy{{^7A3-6zdHr?<^&}8VFrfG@m3%|f~>>(PF)o)UrWCgEN>8Kak+`8AVPWi4{o)7e~*TC z4r}>lXQlND$19U7(($-xBH_IuN$c#Ds5~vxWlhY5OU}u>~@j=tsbp1Sq*!ecF=T5?=bxoOJCYr3?2b@s4(s7FRs}LF~SnC>1|&6Q=Qj&RG(# zfQYAP1D>sO&>F;1X1PgW_=zyQk^rxG9aA6YtJ9(ME)Kb*;qNLhM@k6Ahpe`n6=m{9 zaP7XRdA?RdQ(G&+UwL?M+wufJIDo2bhy$~(l?1bw;wq~kijZ(7;DJa?O% zUERYA&k!RSJ7ey&{80EXH{y=K2x{HzYt3l`kdy*$rjPj1<|ja4yGlHu`9h-6$ddz1 z;LP!^r&A{T9`!zhRpPg0xt4LidDd^c;=c2k(>}6Mi1Np8T*uOXn$PZx{%A;3H-Bw* zrsH1~N{yGm2ilVkJH`Tm$S(!IoM}lz-pU=APxx7RnwomYxX{w4S<6pPGk%$+&JW=4 zt@F~xa-m8xX1_0HZM)m+NmU<&f1i5KVEqrYFbr6JfPK5xr-wqJY%yCis{m4stiik3 zmrw*bR0(0)<&Lig2+neLxLXz-3u;=+#4QXcNBNmc*N3yk?NihCeK*#7Bp>Foq0LT1 zg`9{E!NslKl|+Ki5iI9;eBI0_r>=q%GIna9alKnK0+V?R)#@?5zPoYG=MQ?}Y>e$m zx@gCbilt90#dV1~UDBB$#i38d)>d}Y(g#f@mw64ViU^kIA{^GMp~=@(03ZlU$E^OU zI}jt=IvaOD1yUmT2qu+C8xtUSHJ}aUZ$cQ3jh@tMU$^b<;F1&<_c z>?%YJSxgTtm|(XZ5!*8I++HW2^rE-To3_kFCH;P`*XD_iGi#3|xdi$ytnAa9_I)aLR9` zC?Xx966$oa8SeG*>odk3AQb8iEny3bJwJwca7y!;P@hgUAi@OQi(#Ggi*-!sl}}#x zTiO!GI&C@Q2qqVpk90gfn?|RY6MbPpHutq-D~w)4NIGx;%l-1anPs@+=*Wk3D*%VN z9O-##vh~H$%^INXQ=JeA946W|_PG6Q1VF`)TxSv(u>4fes=T=ZPXARAj2bXM^59ze zaSFQ7v>=%M(tLzjKCEW} zGK2u0l6<1+fgrRli7<@0CR;Z5zdIuHZ%+Z&XW$3qua|Sg8pkUd$%hVH4nU(POBqKY ziAY@M9ml!_8-C~kU^GWyBk{#)KBC!eqT|?qH~id%6+*@`K>wRV?ARZO5V%nZEDS7{ zMYiQF>p@_^KA;LVp(l>`ozC#%SC2!N1|bhD|6V=+euXtHvH+QExqKn>zY zZs`}XR)dR-;IQ=C`eA`wv!O$e;@gyuXjs>HZTP(iE)YU)71| zwd_no?DS6zGd{b2|11Gr{1|lyXxtw>km4d88-ip<;BTx4fbQPhb2+cNA(zaOcwU*Z zpz0GPFZ;lWF9K2gsk_rYv!`QT;GS%AdD}pa=Rp5BA8s0A4#at1>KRKU0q%G<1eLD8 z&D`6?n$YMSJo)MxbXiFnWbb-?4g)0R8%LWPL9JsBNF35Mb#sfw-T@n&%@HI@UvT7q zSI55*N`=aTD`JhHh8;v7zNly+VONX z)Rz3P|3m&8i8Cdpt?+?*ftYJTAF`VPP?J!F!l+FTAFQjouIG;%V3M}s+}{}rp7d+V zc{eYy_T1WN;YKuX0k&|H3 zd>iC5G36%bi!ila7HeA_8M%50Wxb=ntcyI5B3Yki?f%cc_l=O7ySpr`02B*DHBCZI zwZmn&h_9{)A=2^>6jjY4AVUZkdj1&5>}y}V1dOUsZE#t^OThYV*+iw`x6!gy&~G=x zaZ@Tl6ZMsV(}2wN{{N*wOTU!UDBPza*#hLSFqjxN9g)ZUj5JYc54@>PCNP-90&o?e zlD$TChle0~qIE5Bw}UPsLM?ixDXq^DcZYLOr7V^M@&t4OJpr}>y|3)1R zNx^jH{{`Dh1eGc1B^b)~xlv~O``{{ovs2<#dO>zY{9*eCaAHj)m$>mt1pcReyts=@-V>+Oo3|{2Spx-Z0>`}A=zRh>QtDgd|t^{74Pdwpl1c$Ee;`oU+yo;;*5cBJ3R_TgD5!4CTm~Fa; z*=u^f`R!g3F)c8F&@`(ZFbX6xjeqw8f%0i>hYS^vmWu1v%yw0p)H3*miP~o50nXG!eZ{Yk! z+P?n=h%ZPx?i&pPfxCacbfDH1h(0b&4wZ<^y#pD7ZGcgLy}2)+(e&stxrX3fpR``f zcRK(ufBm2SnvEq~quKGrp{i&dR++Vn6nveUVS@aL0JBon%8<66Hv-qu-sb^{8_HURu7HodfA zwg;g2UoW;lAZKQ$7kmQ&=@V?3QpB%*`)1y2kGA|{)4YH{Z0XAxt4GuuV=Gh9t_riD zpd-)WA(0>GMopn8=B{ox5^EOG3&b_Auh+<)H08-2JqiK&702S6y5%YVMrmIu9$A$$ z4Q2lkn8LqS`zwJ({>44>IEPgE-=#n%R~z@md&|qE{lu%^FWR7ar*U4;{@yXc_8;k7 z)2?+oT1)*aU0*4Lf`EQx+AJvC#qI6)fPr8Yu`842Qouc*A`#7ZV)h=)fi9Rf0&|Bx!zo7oT0 z7karH5H_8UCfKgPsiru?{(vfi{Dr_KLaDPX>(gPV@Me!J zsHae6wWN#E4y63mw>bB8L)U6{}%kZRCwfSoG%bCcne-GgHwTQ zcCWc<-1W2%x=aRc`Ya*;goXC2U=;G`dL1OcnZiY1JIzHYIH9+FR^_LTTb^490_Xom zDGDLBRP!dML91vp zd*4{-*^ZBeyLryh&m#I=w%B#cq8&6x z(NVRTeiZHQOh0dRcZhe@2SnTlP7T7kOe3A-PTnn4N&~o}sypixBU#nf_WN7;&!y0}$nwO^m>bvSc(CT~gKy}zzO z6_Y1A8bkaj^wC3SBiD20Rxu1J_6$Rhz%Do=J*8Hxzm?e~N{CA>)EJ>T=$v(cvtN9pfuv}|RfCXhrY z*F+|bE$-yHZ0B@$stJBF9^c!jiy1Ic>ECeMm++z$PgDAL1F3a}XRZ<7(kcBrXbA(e zvXR-qA+01lYMr=+w#@IU+{esxC?JyN?<(GU=lM_T@hwVne>@mrldVsWX5#%6A4a=a z5L3Lm(G_Ap^~18)XI6dq$4KOrd?yy=w6$vdJ+yf@IdfPU23vVw6AT@MVpVm(tsl6H zR37Pt9h{pg6H?NmByNO7rQM5C;nRmRm8CJ!-sh6uYsUD+dYH;Lrm1jF;iPsdt!R}g znkfwvn{1kN6^9?r_!2#%Il^25BHR0qB7gQ4A$M5(B3!NBMZK!i+jJiza)a+qRnd-@ zKu^_|wKB}G&FA8%>YQ%9QZZqJm=gZ(TeWI;*ZZx zOzIOlK=Yk3Z4p57$}`Zr3y$5R;MnyC$8Ib@3DVg-4Id5&3S(Kr2PHBkDpl>gK4n#) zCbZL9J|S91xCV13nssBlkA4vuxiC>w8o92xv?an7Z>pxU0Cr!u0z+`5m%vSgFjgR&`vxzO~vl*sYD1O&( z3MeqF?IfE;qLPZ!tFCR4r)T!oz2wuD8m2`<^jEj`Hgjga8s%}sxMp#T6?^nG?~D*@ zi=*Cx&^ih6gmBDRCa>tI^3ziwFp>j-(Rm=Gs{6$__By<#>(FgeomAFr__Mg9GI|q^ zY}0Bz8`~-013)hwZxvOqWkVS|FWABbu6EOueUK$Q|0CCZ+tUm_j4g4LyncK9%(CVP zGJxn34}tsMjg4T}O3dnBL8=P17jby~qkQp^yE`6KGPL#|Hdv6)w2Kf!2OA}qOKzW; zM4OA4pC%DR@hA&vyC#Cn{>_e3C5}P3lf?481-+Jhb@Cydzi|Gn*TB4oouW7qf3c$X z$IE3;J`hD~1jD_y!loua!Wej=2=W1l;o3pE=B+{qel0&ylzE>{J=;Aike%7Az&Yi0 z%l;>q3QJM@K$wfW*PZZ-Rkr!FLm;Gpx{B;5a})$Lah&6{eZKs9uXwh<&82*+X|mwQ z-8zhw?;0yIrax-J&jshbSnPcvm%|I99*XF%+T!Om={^Oas7w{&79TfXD>Wj3P5B8M zthvH2GCg%50BBbeL5h7Hb`Pk*-hldBxgOMyAR(*v?cM3_Cw!#qwT5A$6Ij2R3q@#l zFyvnsEa-lGcJH5kS-)8iGVAnmW|GMO8Z4tQ1-RxU2Q2%B8kJTt^waOs z_D4AGJ-0ewOa1-2(Hk-&)34+?)Knm;mDO+kD79ngp=!g1m*CqEy7bHV4NIh8oU1BJ z?7k`~ArhdAbX|H#D_duhK&oNtopLO)ooLBOSMkBDb@oq*1EaBfk>??y zp3j~RWxK4OSd-29KyWt9@bwks)6r|hd|rd)`DVSyYZ{{*B~{{+q0WJ{>cGiDgVdGU zz@_K~dj9q-eMSJdVuT+;fnvfeq@>vSa|i|4BisTEk(WFrL_NVN+L2pVqRYb(wRq?E5|dx^xw2U=FWgWq;P#3WWh2y1S#B=*#@BiMkt9|f+?ME&xUrXGovuaaTRfD5> zWzqfAdxFo~;napSf>mIHX?(pKzJ9&?!QxxQlZT65O8Ki;+?1-8uDy#|eczv*(ZpU=+93l>g1mdvOt#(VBmEqfWxR6ja?1kL~( zh_A5Be+h@gqL9{y`TIxUk_IAW9gxL-G?#J(*{_t4sjH=+7bWrEpLHqiB5=oH4rja8 z5<`|wPqWctA<^5*x{uCK5y(Bk>X-bHKke>?AGaMkJ%KGY$!)>+0nF{g~(I| zOQK7;MaWl=8DX2Z4Z#QyxT++(e(EXdRoiazKy+nQEOb!|lbL$t-LNlkyQ@eF=_Rj^ zkF%9QeqYRd^C`2#Ygh3tK#n(;v?oTU15R|+;C|CGRfyb^AL~!iH+x-lsi|$O6<$iH zN}UZ1C+NXoY}yl9!?vM$J5pQd%ht*e;yHo@aV=+@%{uTN#m
RL5xS3{H2_Ianyr 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 void + +/** + * The HTTP method used to send the file to the server in {@link upload}. + */ +enum HttpMethod { + /** + * Send the file using an HTTP `POST` request. This is the default when no method is given. + */ + Post = 'POST', + /** + * Send the file using an HTTP `PUT` request. + */ + Put = 'PUT', + /** + * Send the file using an HTTP `PATCH` request. + */ + Patch = 'PATCH' +} + +/** + * Uploads a file at the given path to a URL, using the file's contents as the request body. + * + * @example + * ```typescript + * import { upload } from '@tauri-apps/plugin-upload'; + * + * await upload( + * 'https://example.com/file-upload', + * './path/to/my/file.txt', + * ({ progress, total }) => console.log(`Uploaded ${progress} of ${total} bytes`), + * { 'Content-Type': 'text/plain' } + * ); + * ``` + * + * @param url The URL to upload the file to. + * @param filePath The path of the file to upload. + * @param progressHandler A callback invoked with upload progress updates. + * @param headers Additional request headers to send with the upload. + * @param method The HTTP method used to send the file. Defaults to {@link HttpMethod.Post}. + * @returns A promise resolving to the response body as text. + * + * @since 2.0.0 + */ +async function upload( + url: string, + filePath: string, + progressHandler?: ProgressHandler, + // TODO: V3 - Combine headers and methods into one `options` object + headers?: Map, + method?: HttpMethod +): Promise { + const ids = new Uint32Array(1) + window.crypto.getRandomValues(ids) + const id = ids[0] + + const onProgress = new Channel() + if (progressHandler) { + onProgress.onmessage = progressHandler + } + + return await invoke('plugin:upload|upload', { + id, + url, + filePath, + headers: headers ?? {}, + method: method ?? HttpMethod.Post, + onProgress + }) +} + +/** + * Downloads a file from a given URL and writes it to the given path on disk. + * + * @example + * ```typescript + * import { download } from '@tauri-apps/plugin-upload'; + * + * await download( + * 'https://example.com/file-download-link', + * './path/to/save/my/file.txt', + * ({ progress, total }) => console.log(`Downloaded ${progress} of ${total} bytes`), + * { 'Content-Type': 'text/plain' } + * ); + * ``` + * + * @param url The URL to download the file from. + * @param filePath The path to save the file to. It must include the file name. + * @param progressHandler A callback invoked with download progress updates. The reported + * `total` will be `0` if the server did not send a `Content-Length` header or the response body + * is compressed. + * @param headers Additional request headers to send with the download request. + * @param body An optional request body. When provided, the download is requested with an HTTP + * `POST` request using this value as the body; otherwise an HTTP `GET` request is used. + * + * @since 2.0.0 + */ +async function download( + url: string, + filePath: string, + progressHandler?: ProgressHandler, + headers?: Map, + body?: string +): Promise { + const ids = new Uint32Array(1) + window.crypto.getRandomValues(ids) + const id = ids[0] + + const onProgress = new Channel() + if (progressHandler) { + onProgress.onmessage = progressHandler + } + + await invoke('plugin:upload|download', { + id, + url, + filePath, + headers: headers ?? {}, + onProgress, + body + }) +} + +export { download, upload, HttpMethod } diff --git a/plugins/upload/package.json b/plugins/upload/package.json new file mode 100644 index 0000000..b9b78d4 --- /dev/null +++ b/plugins/upload/package.json @@ -0,0 +1,30 @@ +{ + "name": "@tauri-apps/plugin-upload", + "version": "2.4.1", + "description": "Upload files from disk to a remote server over HTTP.", + "license": "MIT OR Apache-2.0", + "authors": [ + "Tauri Programme within The Commons Conservancy" + ], + "repository": "https://github.com/tauri-apps/plugins-workspace", + "type": "module", + "types": "./dist-js/index.d.ts", + "main": "./dist-js/index.cjs", + "module": "./dist-js/index.js", + "exports": { + "types": "./dist-js/index.d.ts", + "import": "./dist-js/index.js", + "require": "./dist-js/index.cjs" + }, + "scripts": { + "build": "rollup -c" + }, + "files": [ + "dist-js", + "README.md", + "LICENSE" + ], + "dependencies": { + "@tauri-apps/api": "^2.11.0" + } +} diff --git a/plugins/upload/permissions/autogenerated/commands/download.toml b/plugins/upload/permissions/autogenerated/commands/download.toml new file mode 100644 index 0000000..896b30c --- /dev/null +++ b/plugins/upload/permissions/autogenerated/commands/download.toml @@ -0,0 +1,13 @@ +# Automatically generated - DO NOT EDIT! + +"$schema" = "../../schemas/schema.json" + +[[permission]] +identifier = "allow-download" +description = "Enables the download command without any pre-configured scope." +commands.allow = ["download"] + +[[permission]] +identifier = "deny-download" +description = "Denies the download command without any pre-configured scope." +commands.deny = ["download"] diff --git a/plugins/upload/permissions/autogenerated/commands/upload.toml b/plugins/upload/permissions/autogenerated/commands/upload.toml new file mode 100644 index 0000000..d36081c --- /dev/null +++ b/plugins/upload/permissions/autogenerated/commands/upload.toml @@ -0,0 +1,13 @@ +# Automatically generated - DO NOT EDIT! + +"$schema" = "../../schemas/schema.json" + +[[permission]] +identifier = "allow-upload" +description = "Enables the upload command without any pre-configured scope." +commands.allow = ["upload"] + +[[permission]] +identifier = "deny-upload" +description = "Denies the upload command without any pre-configured scope." +commands.deny = ["upload"] diff --git a/plugins/upload/permissions/autogenerated/reference.md b/plugins/upload/permissions/autogenerated/reference.md new file mode 100644 index 0000000..e8fb99c --- /dev/null +++ b/plugins/upload/permissions/autogenerated/reference.md @@ -0,0 +1,75 @@ +## Default Permission + +This permission set configures what kind of +operations are available from the upload plugin. + +#### Granted Permissions + +All operations are enabled by default. + +#### This default permission set includes the following: + +- `allow-upload` +- `allow-download` + +## Permission Table + + + + + + + + + + + + + + + + + + + + + + + + + + + +
IdentifierDescription
+ +`upload:allow-download` + + + +Enables the download command without any pre-configured scope. + +
+ +`upload:deny-download` + + + +Denies the download command without any pre-configured scope. + +
+ +`upload:allow-upload` + + + +Enables the upload command without any pre-configured scope. + +
+ +`upload:deny-upload` + + + +Denies the upload command without any pre-configured scope. + +
diff --git a/plugins/upload/permissions/default.toml b/plugins/upload/permissions/default.toml new file mode 100644 index 0000000..74a2eb9 --- /dev/null +++ b/plugins/upload/permissions/default.toml @@ -0,0 +1,13 @@ +"$schema" = "schemas/schema.json" + +[default] +description = """ +This permission set configures what kind of +operations are available from the upload plugin. + +#### Granted Permissions + +All operations are enabled by default. + +""" +permissions = ["allow-upload", "allow-download"] diff --git a/plugins/upload/permissions/schemas/schema.json b/plugins/upload/permissions/schemas/schema.json new file mode 100644 index 0000000..8b52464 --- /dev/null +++ b/plugins/upload/permissions/schemas/schema.json @@ -0,0 +1,330 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "PermissionFile", + "description": "Permission file that can define a default permission, a set of permissions or a list of inlined permissions.", + "type": "object", + "properties": { + "default": { + "description": "The default permission set for the plugin", + "anyOf": [ + { + "$ref": "#/definitions/DefaultPermission" + }, + { + "type": "null" + } + ] + }, + "set": { + "description": "A list of permissions sets defined", + "type": "array", + "items": { + "$ref": "#/definitions/PermissionSet" + } + }, + "permission": { + "description": "A list of inlined permissions", + "default": [], + "type": "array", + "items": { + "$ref": "#/definitions/Permission" + } + } + }, + "definitions": { + "DefaultPermission": { + "description": "The default permission set of the plugin.\n\nWorks similarly to a permission with the \"default\" identifier.", + "type": "object", + "required": [ + "permissions" + ], + "properties": { + "version": { + "description": "The version of the permission.", + "type": [ + "integer", + "null" + ], + "format": "uint64", + "minimum": 1.0 + }, + "description": { + "description": "Human-readable description of what the permission does. Tauri convention is to use `

` headings in markdown content for Tauri documentation generation purposes.", + "type": [ + "string", + "null" + ] + }, + "permissions": { + "description": "All permissions this set contains.", + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "PermissionSet": { + "description": "A set of direct permissions grouped together under a new name.", + "type": "object", + "required": [ + "description", + "identifier", + "permissions" + ], + "properties": { + "identifier": { + "description": "A unique identifier for the permission.", + "type": "string" + }, + "description": { + "description": "Human-readable description of what the permission does.", + "type": "string" + }, + "permissions": { + "description": "All permissions this set contains.", + "type": "array", + "items": { + "$ref": "#/definitions/PermissionKind" + } + } + } + }, + "Permission": { + "description": "Descriptions of explicit privileges of commands.\n\nIt can enable commands to be accessible in the frontend of the application.\n\nIf the scope is defined it can be used to fine grain control the access of individual or multiple commands.", + "type": "object", + "required": [ + "identifier" + ], + "properties": { + "version": { + "description": "The version of the permission.", + "type": [ + "integer", + "null" + ], + "format": "uint64", + "minimum": 1.0 + }, + "identifier": { + "description": "A unique identifier for the permission.", + "type": "string" + }, + "description": { + "description": "Human-readable description of what the permission does. Tauri internal convention is to use `

` headings in markdown content for Tauri documentation generation purposes.", + "type": [ + "string", + "null" + ] + }, + "commands": { + "description": "Allowed or denied commands when using this permission.", + "default": { + "allow": [], + "deny": [] + }, + "allOf": [ + { + "$ref": "#/definitions/Commands" + } + ] + }, + "scope": { + "description": "Allowed or denied scoped when using this permission.", + "allOf": [ + { + "$ref": "#/definitions/Scopes" + } + ] + }, + "platforms": { + "description": "Target platforms this permission applies. By default all platforms are affected by this permission.", + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/Target" + } + } + } + }, + "Commands": { + "description": "Allowed and denied commands inside a permission.\n\nIf two commands clash inside of `allow` and `deny`, it should be denied by default.", + "type": "object", + "properties": { + "allow": { + "description": "Allowed command.", + "default": [], + "type": "array", + "items": { + "type": "string" + } + }, + "deny": { + "description": "Denied command, which takes priority.", + "default": [], + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "Scopes": { + "description": "An argument for fine grained behavior control of Tauri commands.\n\nIt can be of any serde serializable type and is used to allow or prevent certain actions inside a Tauri command. The configured scope is passed to the command and will be enforced by the command implementation.\n\n## Example\n\n```json { \"allow\": [{ \"path\": \"$HOME/**\" }], \"deny\": [{ \"path\": \"$HOME/secret.txt\" }] } ```", + "type": "object", + "properties": { + "allow": { + "description": "Data that defines what is allowed by the scope.", + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/Value" + } + }, + "deny": { + "description": "Data that defines what is denied by the scope. This should be prioritized by validation logic.", + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/Value" + } + } + } + }, + "Value": { + "description": "All supported ACL values.", + "anyOf": [ + { + "description": "Represents a null JSON value.", + "type": "null" + }, + { + "description": "Represents a [`bool`].", + "type": "boolean" + }, + { + "description": "Represents a valid ACL [`Number`].", + "allOf": [ + { + "$ref": "#/definitions/Number" + } + ] + }, + { + "description": "Represents a [`String`].", + "type": "string" + }, + { + "description": "Represents a list of other [`Value`]s.", + "type": "array", + "items": { + "$ref": "#/definitions/Value" + } + }, + { + "description": "Represents a map of [`String`] keys to [`Value`]s.", + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/Value" + } + } + ] + }, + "Number": { + "description": "A valid ACL number.", + "anyOf": [ + { + "description": "Represents an [`i64`].", + "type": "integer", + "format": "int64" + }, + { + "description": "Represents a [`f64`].", + "type": "number", + "format": "double" + } + ] + }, + "Target": { + "description": "Platform target.", + "oneOf": [ + { + "description": "MacOS.", + "type": "string", + "enum": [ + "macOS" + ] + }, + { + "description": "Windows.", + "type": "string", + "enum": [ + "windows" + ] + }, + { + "description": "Linux.", + "type": "string", + "enum": [ + "linux" + ] + }, + { + "description": "Android.", + "type": "string", + "enum": [ + "android" + ] + }, + { + "description": "iOS.", + "type": "string", + "enum": [ + "iOS" + ] + } + ] + }, + "PermissionKind": { + "type": "string", + "oneOf": [ + { + "description": "Enables the download command without any pre-configured scope.", + "type": "string", + "const": "allow-download", + "markdownDescription": "Enables the download command without any pre-configured scope." + }, + { + "description": "Denies the download command without any pre-configured scope.", + "type": "string", + "const": "deny-download", + "markdownDescription": "Denies the download command without any pre-configured scope." + }, + { + "description": "Enables the upload command without any pre-configured scope.", + "type": "string", + "const": "allow-upload", + "markdownDescription": "Enables the upload command without any pre-configured scope." + }, + { + "description": "Denies the upload command without any pre-configured scope.", + "type": "string", + "const": "deny-upload", + "markdownDescription": "Denies the upload command without any pre-configured scope." + }, + { + "description": "This permission set configures what kind of\noperations are available from the upload plugin.\n\n#### Granted Permissions\n\nAll operations are enabled by default.\n\n\n#### This default permission set includes:\n\n- `allow-upload`\n- `allow-download`", + "type": "string", + "const": "default", + "markdownDescription": "This permission set configures what kind of\noperations are available from the upload plugin.\n\n#### Granted Permissions\n\nAll operations are enabled by default.\n\n\n#### This default permission set includes:\n\n- `allow-upload`\n- `allow-download`" + } + ] + } + } +} \ No newline at end of file diff --git a/plugins/upload/rollup.config.js b/plugins/upload/rollup.config.js new file mode 100644 index 0000000..1f349ec --- /dev/null +++ b/plugins/upload/rollup.config.js @@ -0,0 +1,7 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +import { createConfig } from '../../shared/rollup.config.js' + +export default createConfig() diff --git a/plugins/upload/src/lib.rs b/plugins/upload/src/lib.rs new file mode 100644 index 0000000..7cffb35 --- /dev/null +++ b/plugins/upload/src/lib.rs @@ -0,0 +1,380 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +//! Upload files from disk to a remote server over HTTP. +//! +//! Download files from a remote HTTP server to disk. +//! +//! ## Cargo features +//! +//! - **rustls-tls** *(enabled by default)*: Enables TLS functionality provided by `rustls`. +//! - **native-tls**: Enables TLS functionality provided by `native-tls`. +//! - **native-tls-vendored**: Enables the `vendored` feature of `native-tls`. + +#![doc( + html_logo_url = "https://github.com/tauri-apps/tauri/raw/dev/app-icon.png", + html_favicon_url = "https://github.com/tauri-apps/tauri/raw/dev/app-icon.png" +)] + +mod transfer_stats; +use transfer_stats::TransferStats; + +use futures_util::TryStreamExt; +use serde::{ser::Serializer, Deserialize, Serialize}; +use tauri::{ + command, + ipc::Channel, + plugin::{Builder as PluginBuilder, TauriPlugin}, + Runtime, +}; +use tokio::{ + fs::File, + io::{AsyncWriteExt, BufWriter}, +}; +use tokio_util::codec::{BytesCodec, FramedRead}; + +use read_progress_stream::ReadProgressStream; + +use std::collections::HashMap; + +/// The HTTP method used to send the file in the `upload` command. +/// +/// Serialized as an uppercase string (`"POST"`, `"PUT"` or `"PATCH"`) to match the JavaScript +/// guest bindings. +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "UPPERCASE")] +pub enum HttpMethod { + /// Send the file with an HTTP `POST` request. This is the default when no method is given. + Post, + /// Send the file with an HTTP `PUT` request. + Put, + /// Send the file with an HTTP `PATCH` request. + Patch, +} + +type Result = std::result::Result; + +/// The error type returned by this plugin's `upload` and `download` commands. +#[derive(Debug, thiserror::Error)] +pub enum Error { + /// An I/O error, for example failing to open, read or write the file at the given path, or + /// the background task that performs the transfer panicking. + #[error(transparent)] + Io(#[from] std::io::Error), + /// An error returned by the underlying [`reqwest`] HTTP client while sending the request or + /// reading the response. + #[error(transparent)] + Request(#[from] reqwest::Error), + /// The content length of the request or response could not be determined. + #[error("{0}")] + ContentLength(String), + /// The HTTP response did not indicate success. Contains the status code and the response + /// body text. + #[error("request failed with status code {0}: {1}")] + HttpErrorCode(u16, String), +} + +impl Serialize for Error { + fn serialize(&self, serializer: S) -> std::result::Result + where + S: Serializer, + { + serializer.serialize_str(self.to_string().as_ref()) + } +} + +#[derive(Clone, Serialize)] +#[serde(rename_all = "camelCase")] +struct ProgressPayload { + progress: u64, + progress_total: u64, + total: u64, + transfer_speed: u64, +} + +#[command] +async fn download( + url: String, + file_path: String, + headers: HashMap, + body: Option, + on_progress: Channel, +) -> Result<()> { + tokio::spawn(async move { + let client = reqwest::Client::new(); + let mut request = if let Some(body) = body { + client.post(&url).body(body) + } else { + client.get(&url) + }; + // Loop trought the headers keys and values + // and add them to the request object. + for (key, value) in headers { + request = request.header(&key, value); + } + + let response = request.send().await?; + if !response.status().is_success() { + return Err(Error::HttpErrorCode( + response.status().as_u16(), + response.text().await.unwrap_or_default(), + )); + } + let total = response.content_length().unwrap_or(0); + + let mut file = BufWriter::new(File::create(&file_path).await?); + let mut stream = response.bytes_stream(); + + let mut stats = TransferStats::default(); + while let Some(chunk) = stream.try_next().await? { + file.write_all(&chunk).await?; + stats.record_chunk_transfer(chunk.len()); + let _ = on_progress.send(ProgressPayload { + progress: chunk.len() as u64, + progress_total: stats.total_transferred, + total, + transfer_speed: stats.transfer_speed, + }); + } + file.flush().await?; + Ok(()) + }) + .await + .map_err(|e| Error::Io(std::io::Error::other(e.to_string())))? +} + +#[command] +async fn upload( + url: String, + file_path: String, + headers: HashMap, + method: Option, + on_progress: Channel, +) -> Result { + tokio::spawn(async move { + // Read the file + let file = File::open(&file_path).await?; + let file_len = file.metadata().await.unwrap().len(); + + // Get HTTP method (defaults to POST) + let http_method = method.unwrap_or(HttpMethod::Post); + + // Create the request and attach the file to the body + let client = reqwest::Client::new(); + let mut request = match http_method { + HttpMethod::Put => client.put(&url), + HttpMethod::Patch => client.patch(&url), + HttpMethod::Post => client.post(&url), + } + .header(reqwest::header::CONTENT_LENGTH, file_len) + .body(file_to_body(on_progress, file, file_len)); + + // Loop through the headers keys and values + // and add them to the request object. + for (key, value) in headers { + request = request.header(&key, value); + } + + let response = request.send().await?; + if response.status().is_success() { + response.text().await.map_err(Into::into) + } else { + Err(Error::HttpErrorCode( + response.status().as_u16(), + response.text().await.unwrap_or_default(), + )) + } + }) + .await + .map_err(|e| Error::Io(std::io::Error::other(e.to_string())))? +} + +fn file_to_body(channel: Channel, file: File, file_len: u64) -> reqwest::Body { + let stream = FramedRead::new(file, BytesCodec::new()).map_ok(|r| r.freeze()); + + let mut stats = TransferStats::default(); + reqwest::Body::wrap_stream(ReadProgressStream::new( + stream, + Box::new(move |progress, _total| { + stats.record_chunk_transfer(progress as usize); + let _ = channel.send(ProgressPayload { + progress, + progress_total: stats.total_transferred, + total: file_len, + transfer_speed: stats.transfer_speed, + }); + }), + )) +} + +/// Initializes the upload plugin, registering the `upload` and `download` commands. +pub fn init() -> TauriPlugin { + PluginBuilder::new("upload") + .invoke_handler(tauri::generate_handler![download, upload]) + .build() +} + +#[cfg(test)] +mod tests { + use super::*; + use mockito::{self, Mock, Server, ServerGuard}; + use tauri::ipc::InvokeResponseBody; + struct MockedServer { + _server: ServerGuard, + url: String, + mocked_endpoint: Mock, + } + + #[tokio::test] + async fn should_error_on_download_if_status_not_success() { + let mocked_server = spawn_server_mocked(400).await; + let result = download_file(mocked_server.url).await; + mocked_server.mocked_endpoint.assert(); + assert!(result.is_err()); + } + + #[tokio::test] + async fn should_download_file_successfully() { + let mocked_server = spawn_server_mocked(200).await; + let result = download_file(mocked_server.url).await; + mocked_server.mocked_endpoint.assert(); + assert!( + result.is_ok(), + "failed to download file: {}", + result.unwrap_err() + ); + } + + #[tokio::test] + async fn should_error_on_upload_if_status_not_success() { + let mocked_server = spawn_upload_server_mocked(500, "POST").await; + let result = upload_file(mocked_server.url, None).await; + mocked_server.mocked_endpoint.assert(); + assert!(result.is_err()); + match result.unwrap_err() { + Error::HttpErrorCode(status, _) => assert_eq!(status, 500), + _ => panic!("Expected HttpErrorCode error"), + } + } + + #[tokio::test] + async fn should_error_on_upload_if_file_not_found() { + let mocked_server = spawn_upload_server_mocked(200, "POST").await; + let file_path = "/nonexistent/file.txt".to_string(); + let headers = HashMap::new(); + let sender: Channel = + Channel::new(|msg: InvokeResponseBody| -> tauri::Result<()> { + let _ = msg; + Ok(()) + }); + + let result = upload(mocked_server.url, file_path, headers, None, sender).await; + assert!(result.is_err()); + match result.unwrap_err() { + Error::Io(_) => {} + _ => panic!("Expected IO error for missing file"), + } + } + + #[tokio::test] + async fn should_upload_file_with_post_method() { + let mocked_server = spawn_upload_server_mocked(200, "POST").await; + let result = upload_file(mocked_server.url, Some(HttpMethod::Post)).await; + mocked_server.mocked_endpoint.assert(); + assert!( + result.is_ok(), + "failed to upload file: {}", + result.unwrap_err() + ); + let response_body = result.unwrap(); + assert_eq!(response_body, "upload successful"); + } + + #[tokio::test] + async fn should_upload_file_with_put_method() { + let mocked_server = spawn_upload_server_mocked(200, "PUT").await; + let result = upload_file(mocked_server.url, Some(HttpMethod::Put)).await; + mocked_server.mocked_endpoint.assert(); + assert!( + result.is_ok(), + "failed to upload file with PUT: {}", + result.unwrap_err() + ); + let response_body = result.unwrap(); + assert_eq!(response_body, "upload successful"); + } + + #[tokio::test] + async fn should_upload_file_with_patch_method() { + let mocked_server = spawn_upload_server_mocked(200, "PATCH").await; + let result = upload_file(mocked_server.url, Some(HttpMethod::Patch)).await; + mocked_server.mocked_endpoint.assert(); + assert!( + result.is_ok(), + "failed to upload file with PATCH: {}", + result.unwrap_err() + ); + let response_body = result.unwrap(); + assert_eq!(response_body, "upload successful"); + } + + async fn download_file(url: String) -> Result<()> { + let file_path = concat!(env!("CARGO_MANIFEST_DIR"), "/test/download.txt").to_string(); + let headers = HashMap::new(); + let sender: Channel = + Channel::new(|msg: InvokeResponseBody| -> tauri::Result<()> { + let _ = msg; + Ok(()) + }); + download(url, file_path, headers, None, sender).await + } + + async fn upload_file(url: String, method: Option) -> Result { + let file_path = concat!(env!("CARGO_MANIFEST_DIR"), "/test/upload.txt").to_string(); + let headers = HashMap::new(); + let sender: Channel = + Channel::new(|msg: InvokeResponseBody| -> tauri::Result<()> { + let _ = msg; + Ok(()) + }); + upload(url, file_path, headers, method, sender).await + } + + async fn spawn_server_mocked(return_status: usize) -> MockedServer { + let mut _server = Server::new_async().await; + let path = "/mock_test"; + let mock = _server + .mock("GET", path) + .with_status(return_status) + .with_body("mocked response body") + .create_async() + .await; + + let url = _server.url() + path; + MockedServer { + _server, + url, + mocked_endpoint: mock, + } + } + + async fn spawn_upload_server_mocked(return_status: usize, method: &str) -> MockedServer { + let mut _server = Server::new_async().await; + let path = "/upload_test"; + let mock = _server + .mock(method, path) + .with_status(return_status) + .with_body("upload successful") + .match_header("content-length", "20") + .create_async() + .await; + + let url = _server.url() + path; + MockedServer { + _server, + url, + mocked_endpoint: mock, + } + } +} diff --git a/plugins/upload/src/transfer_stats.rs b/plugins/upload/src/transfer_stats.rs new file mode 100644 index 0000000..2f3a394 --- /dev/null +++ b/plugins/upload/src/transfer_stats.rs @@ -0,0 +1,55 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use std::time::Instant; + +// The TransferStats struct tracks both transfer speed and cumulative transfer progress. +pub struct TransferStats { + accumulated_chunk_len: usize, // Total length of chunks transferred in the current period + accumulated_time: u128, // Total time taken for the transfers in the current period + pub transfer_speed: u64, // Calculated transfer speed in bytes per second + pub total_transferred: u64, // Cumulative total of all transferred data + start_time: Instant, // Time when the current period started + granularity: u32, // Time period (in milliseconds) over which the transfer speed is calculated +} + +impl TransferStats { + // Initializes a new TransferStats instance with the specified granularity. + pub fn start(granularity: u32) -> Self { + Self { + accumulated_chunk_len: 0, + accumulated_time: 0, + transfer_speed: 0, + total_transferred: 0, + start_time: Instant::now(), + granularity, + } + } + // Records the transfer of a data chunk and updates both transfer speed and total progress. + pub fn record_chunk_transfer(&mut self, chunk_len: usize) { + let now = Instant::now(); + let it_took = now.duration_since(self.start_time).as_millis(); + self.accumulated_chunk_len += chunk_len; + self.total_transferred += chunk_len as u64; + self.accumulated_time += it_took; + + // Calculate transfer speed if accumulated time exceeds granularity. + if self.accumulated_time >= self.granularity as u128 { + self.transfer_speed = + (self.accumulated_chunk_len as u128 / self.accumulated_time * 1024) as u64; + self.accumulated_chunk_len = 0; + self.accumulated_time = 0; + } + + // Reset the start time for the next period. + self.start_time = now; + } +} + +// Provides a default implementation for TransferStats with a granularity of 500 milliseconds. +impl Default for TransferStats { + fn default() -> Self { + Self::start(500) // Default granularity is 500 ms + } +} diff --git a/plugins/upload/test/download.txt b/plugins/upload/test/download.txt new file mode 100644 index 0000000..629b997 --- /dev/null +++ b/plugins/upload/test/download.txt @@ -0,0 +1 @@ +mocked response body \ No newline at end of file diff --git a/plugins/upload/test/upload.txt b/plugins/upload/test/upload.txt new file mode 100644 index 0000000..629b997 --- /dev/null +++ b/plugins/upload/test/upload.txt @@ -0,0 +1 @@ +mocked response body \ No newline at end of file diff --git a/plugins/upload/tsconfig.json b/plugins/upload/tsconfig.json new file mode 100644 index 0000000..4374468 --- /dev/null +++ b/plugins/upload/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "rootDir": "guest-js" + }, + "include": ["guest-js/*.ts"] +} diff --git a/plugins/websocket/CHANGELOG.md b/plugins/websocket/CHANGELOG.md new file mode 100644 index 0000000..bfb94e1 --- /dev/null +++ b/plugins/websocket/CHANGELOG.md @@ -0,0 +1,126 @@ +# Changelog + +## [2.4.3] + +- [`f8053e65`](https://github.com/tauri-apps/plugins-workspace/commit/f8053e659e4ccd85c1f52833411ff8417cbc5e69) ([#3527](https://github.com/tauri-apps/plugins-workspace/pull/3527) by [@Legend-Master](https://github.com/tauri-apps/plugins-workspace/../../Legend-Master)) Documented Cargo feature flags in each plugin's crate-level documentation. + +## \[2.4.2] + +### bug + +- [`66a75ece`](https://github.com/tauri-apps/plugins-workspace/commit/66a75ece27ee2f4e7364ef9ddc8db6cbeeb56919) ([#3124](https://github.com/tauri-apps/plugins-workspace/pull/3124) by [@FabianLars](https://github.com/tauri-apps/plugins-workspace/../../FabianLars)) The WebSocket plugin will now install the default crypto provider if needed, preventing panics on WSS connections. + +## \[2.4.1] + +- [`93426f85`](https://github.com/tauri-apps/plugins-workspace/commit/93426f85120f49beb9f40222bff45185a32d54a9) Fixed an issue that caused docs.rs builds to fail. No user facing changes. + +## \[2.4.0] + +- [`f209b2f2`](https://github.com/tauri-apps/plugins-workspace/commit/f209b2f23cb29133c97ad5961fb46ef794dbe063) ([#2804](https://github.com/tauri-apps/plugins-workspace/pull/2804) by [@renovate](https://github.com/tauri-apps/plugins-workspace/../../renovate)) Updated tauri to 2.6 + +## \[2.3.1] + +- [`f634e524`](https://github.com/tauri-apps/plugins-workspace/commit/f634e5248ebe428f8305a59f74c13fc15147fb8e) This is an "empty" release to update the plugins' source files on crates.io and docs.rs. This should fix docs.rs build failures for projects using tauri plugins as dependencies. + +## \[2.3.0] + +- [`78acfa45`](https://github.com/tauri-apps/plugins-workspace/commit/78acfa456f343c0af5fbf35660802d14ff7a5eca) ([#2027](https://github.com/tauri-apps/plugins-workspace/pull/2027) by [@twlite](https://github.com/tauri-apps/plugins-workspace/../../twlite)) `addListener` now returns a cleanup function instead of `void` to remove the listener. + +## \[2.2.1] + +- [`05cca602`](https://github.com/tauri-apps/plugins-workspace/commit/05cca602d927c30014d3892438ca28d99bc4e1d3) ([#2210](https://github.com/tauri-apps/plugins-workspace/pull/2210) by [@renovate](https://github.com/tauri-apps/plugins-workspace/../../renovate)) **Breaking change:** Updated tokio_tungstenite to `0.26`. This may be a breaking change if you use the `tls_connector` Builder method in Rust. + **Breaking change:** Removed the accidental `ConnectionConfig` struct re-export (in rust). This should not affect anyone since the Config was not used in any public API. + +## \[2.2.0] + +- [`3a79266b`](https://github.com/tauri-apps/plugins-workspace/commit/3a79266b8cf96a55b1ae6339d725567d45a44b1d) ([#2173](https://github.com/tauri-apps/plugins-workspace/pull/2173) by [@FabianLars](https://github.com/tauri-apps/plugins-workspace/../../FabianLars)) Bumped all plugins to `v2.2.0`. From now, the versions for the Rust and JavaScript packages of each plugin will be in sync with each other. + +## \[2.0.1] + +- [`a1a82208`](https://github.com/tauri-apps/plugins-workspace/commit/a1a82208ed4ab87f83310be0dc95428aec9ab241) ([#1873](https://github.com/tauri-apps/plugins-workspace/pull/1873) by [@lucasfernog](https://github.com/tauri-apps/plugins-workspace/../../lucasfernog)) Downgrade MSRV to 1.77.2 to support Windows 7. + +## \[2.0.0] + +- [`e2c4dfb6`](https://github.com/tauri-apps/plugins-workspace/commit/e2c4dfb6af43e5dd8d9ceba232c315f5febd55c1) Update to tauri v2 stable release. + +## \[2.0.0-rc.1] + +- [`e2e97db5`](https://github.com/tauri-apps/plugins-workspace/commit/e2e97db51983267f5be84d4f6f0278d58834d1f5) ([#1701](https://github.com/tauri-apps/plugins-workspace/pull/1701) by [@lucasfernog](https://github.com/tauri-apps/plugins-workspace/../../lucasfernog)) Update to tauri 2.0.0-rc.8 + +## \[2.0.0-rc.0] + +- [`9887d1`](https://github.com/tauri-apps/plugins-workspace/commit/9887d14bd0e971c4c0f5c1188fc4005d3fc2e29e) Update to tauri RC. + +## \[2.0.0-beta.8] + +- [`99d6ac0f`](https://github.com/tauri-apps/plugins-workspace/commit/99d6ac0f9506a6a4a1aa59c728157190a7441af6) ([#1606](https://github.com/tauri-apps/plugins-workspace/pull/1606) by [@FabianLars](https://github.com/tauri-apps/plugins-workspace/../../FabianLars)) The JS packages now specify the *minimum* `@tauri-apps/api` version instead of a single exact version. +- [`6de87966`](https://github.com/tauri-apps/plugins-workspace/commit/6de87966ecc00ad9d91c25be452f1f46bd2b7e1f) ([#1597](https://github.com/tauri-apps/plugins-workspace/pull/1597) by [@Legend-Master](https://github.com/tauri-apps/plugins-workspace/../../Legend-Master)) Update to tauri beta.25. + +## \[2.0.0-beta.7] + +- [`22a17980`](https://github.com/tauri-apps/plugins-workspace/commit/22a17980ff4f6f8c40adb1b8f4ffc6dae2fe7e30) ([#1537](https://github.com/tauri-apps/plugins-workspace/pull/1537) by [@lucasfernog](https://github.com/tauri-apps/plugins-workspace/../../lucasfernog)) Update to tauri beta.24. + +## \[2.0.0-beta.6] + +- [`76daee7a`](https://github.com/tauri-apps/plugins-workspace/commit/76daee7aafece34de3092c86e531cf9eb1138989) ([#1512](https://github.com/tauri-apps/plugins-workspace/pull/1512) by [@renovate](https://github.com/tauri-apps/plugins-workspace/../../renovate)) Update to tauri beta.23. + +## \[2.0.0-beta.5] + +- [`9013854f`](https://github.com/tauri-apps/plugins-workspace/commit/9013854f42a49a230b9dbb9d02774765528a923f)([#1382](https://github.com/tauri-apps/plugins-workspace/pull/1382)) Update to tauri beta.22. + +## \[2.0.0-beta.4] + +- [`430bd6f4`](https://github.com/tauri-apps/plugins-workspace/commit/430bd6f4f379bee5d232ae6b098ae131db7f178a)([#1363](https://github.com/tauri-apps/plugins-workspace/pull/1363)) Update to tauri beta.20. + +## \[2.0.0-beta.3] + +- [`bd1ed590`](https://github.com/tauri-apps/plugins-workspace/commit/bd1ed5903ffcce5500310dac1e59e8c67674ef1e)([#1237](https://github.com/tauri-apps/plugins-workspace/pull/1237)) Update to tauri beta.17. + +## \[2.0.0-beta.4] + +- [`ed46dca`](https://github.com/tauri-apps/plugins-workspace/commit/ed46dca74ff3947dbbcb26a7b571c129bf925698) **Breaking change:** Enable rustls by default and added a method to configure the TLS Connector for tungstenite. + +## \[2.0.0-beta.3] + +- [`a04ea2f`](https://github.com/tauri-apps/plugins-workspace/commit/a04ea2f38294d5a3987578283badc8eec87a7752)([#1071](https://github.com/tauri-apps/plugins-workspace/pull/1071)) The global API script is now only added to the binary when the `withGlobalTauri` config is true. + +## \[2.0.0-beta.2] + +- [`99bea25`](https://github.com/tauri-apps/plugins-workspace/commit/99bea2559c2c0648c2519c50a18cd124dacef57b)([#1005](https://github.com/tauri-apps/plugins-workspace/pull/1005)) Update to tauri beta.8. + +## \[2.0.0-beta.1] + +- [`569defb`](https://github.com/tauri-apps/plugins-workspace/commit/569defbe9492e38938554bb7bdc1be9151456d21) Update to tauri beta.4. + +## \[2.0.0-beta.0] + +- [`d198c01`](https://github.com/tauri-apps/plugins-workspace/commit/d198c014863ee260cb0de88a14b7fc4356ef7474)([#862](https://github.com/tauri-apps/plugins-workspace/pull/862)) Update to tauri beta. + +## \[2.0.0-alpha.5] + +- [`387c2f9`](https://github.com/tauri-apps/plugins-workspace/commit/387c2f9e0ce4c75c07ffa3fd76391a25b58f5daf)([#802](https://github.com/tauri-apps/plugins-workspace/pull/802)) Update to @tauri-apps/api v2.0.0-alpha.13. + +## \[2.0.0-alpha.4] + +- [`387c2f9`](https://github.com/tauri-apps/plugins-workspace/commit/387c2f9e0ce4c75c07ffa3fd76391a25b58f5daf)([#802](https://github.com/tauri-apps/plugins-workspace/pull/802)) Update to @tauri-apps/api v2.0.0-alpha.12. +- [`251852c`](https://github.com/tauri-apps/plugins-workspace/commit/251852ccbc97abe5765fb9663aab27701f3d7c7c)([#702](https://github.com/tauri-apps/plugins-workspace/pull/702)) Add support for custom request headers. + +## \[2.0.0-alpha.3] + +- [`e438e0a`](https://github.com/tauri-apps/plugins-workspace/commit/e438e0a62d4b430a5159f05f13ecd397dd891a0d)([#676](https://github.com/tauri-apps/plugins-workspace/pull/676)) Update to @tauri-apps/api v2.0.0-alpha.11. + +## \[2.0.0-alpha.2] + +- [`5c13736`](https://github.com/tauri-apps/plugins-workspace/commit/5c137365c60790e8d4037d449e8237aa3fffdab0)([#673](https://github.com/tauri-apps/plugins-workspace/pull/673)) Update to @tauri-apps/api v2.0.0-alpha.9. + +## \[2.0.0-alpha.2] + +- [`4e2cef9`](https://github.com/tauri-apps/plugins-workspace/commit/4e2cef9b702bbbb9cf4ee17de50791cb21f1b2a4)([#593](https://github.com/tauri-apps/plugins-workspace/pull/593)) Update to alpha.12. + +## \[2.0.0-alpha.1] + +- [`d74fc0a`](https://github.com/tauri-apps/plugins-workspace/commit/d74fc0a097996e90a37be8f57d50b7d1f6ca616f)([#555](https://github.com/tauri-apps/plugins-workspace/pull/555)) Update to alpha.11. + +## \[2.0.0-alpha.0] + +- [`717ae67`](https://github.com/tauri-apps/plugins-workspace/commit/717ae670978feb4492fac1f295998b93f2b9347f)([#371](https://github.com/tauri-apps/plugins-workspace/pull/371)) First v2 alpha release! diff --git a/plugins/websocket/Cargo.toml b/plugins/websocket/Cargo.toml new file mode 100644 index 0000000..1e256d2 --- /dev/null +++ b/plugins/websocket/Cargo.toml @@ -0,0 +1,53 @@ +[package] +name = "tauri-plugin-websocket" +version = "2.4.3" +description = "Expose a WebSocket server to your Tauri frontend." +authors = { workspace = true } +license = { workspace = true } +edition = { workspace = true } +rust-version = { workspace = true } +repository = { workspace = true } +links = "tauri-plugin-websocket" +exclude = [ + "/banner.png", + "/examples", + "/guest-js", + "/package.json", + "/rollup.config.js", + "/tsconfig.json", +] + +[package.metadata.platforms.support] +windows = { level = "full", notes = "" } +linux = { level = "full", notes = "" } +macos = { level = "full", notes = "" } +android = { level = "full", notes = "" } +ios = { level = "full", notes = "" } + +[build-dependencies] +tauri-plugin = { workspace = true, features = ["build"] } + +[dependencies] +serde = { workspace = true } +serde_json = { workspace = true } +tauri = { workspace = true } +log = { workspace = true } +thiserror = { workspace = true } +http = "1" +rand = "0.9" +futures-util = "0.3" +tokio = { version = "1", features = ["net", "sync"] } +tokio-tungstenite = { version = "0.29" } +rustls = { version = "0.23", default-features = false, features = [ + "ring", +], optional = true } + +[features] +default = ["rustls-tls"] +native-tls = ["tokio-tungstenite/native-tls"] +native-tls-vendored = ["native-tls", "tokio-tungstenite/native-tls-vendored"] +rustls-tls = ["tokio-tungstenite/rustls-tls-webpki-roots", "dep:rustls"] +rustls-tls-native-roots = [ + "tokio-tungstenite/rustls-tls-native-roots", + "dep:rustls", +] diff --git a/plugins/websocket/LICENSE.spdx b/plugins/websocket/LICENSE.spdx new file mode 100644 index 0000000..cdd0df5 --- /dev/null +++ b/plugins/websocket/LICENSE.spdx @@ -0,0 +1,20 @@ +SPDXVersion: SPDX-2.1 +DataLicense: CC0-1.0 +PackageName: tauri +DataFormat: SPDXRef-1 +PackageSupplier: Organization: The Tauri Programme in the Commons Conservancy +PackageHomePage: https://tauri.app +PackageLicenseDeclared: Apache-2.0 +PackageLicenseDeclared: MIT +PackageCopyrightText: 2019-2022, The Tauri Programme in the Commons Conservancy +PackageSummary: Tauri is a rust project that enables developers to make secure +and small desktop applications using a web frontend. + +PackageComment: The package includes the following libraries; see +Relationship information. + +Created: 2019-05-20T09:00:00Z +PackageDownloadLocation: git://github.com/tauri-apps/tauri +PackageDownloadLocation: git+https://github.com/tauri-apps/tauri.git +PackageDownloadLocation: git+ssh://github.com/tauri-apps/tauri.git +Creator: Person: Daniel Thompson-Yvetot \ No newline at end of file diff --git a/plugins/websocket/LICENSE_APACHE-2.0 b/plugins/websocket/LICENSE_APACHE-2.0 new file mode 100644 index 0000000..4947287 --- /dev/null +++ b/plugins/websocket/LICENSE_APACHE-2.0 @@ -0,0 +1,177 @@ + + 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 \ No newline at end of file diff --git a/plugins/websocket/LICENSE_MIT b/plugins/websocket/LICENSE_MIT new file mode 100644 index 0000000..4d75472 --- /dev/null +++ b/plugins/websocket/LICENSE_MIT @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2017 - Present Tauri Apps 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. \ No newline at end of file diff --git a/plugins/websocket/README.md b/plugins/websocket/README.md new file mode 100644 index 0000000..3d9e7a8 --- /dev/null +++ b/plugins/websocket/README.md @@ -0,0 +1,95 @@ +![plugin-websocket](https://github.com/tauri-apps/plugins-workspace/raw/v2/plugins/websocket/banner.png) + +Open a WebSocket connection using a Rust client in JS. + +| Platform | Supported | +| -------- | --------- | +| Linux | ✓ | +| Windows | ✓ | +| macOS | ✓ | +| Android | ✓ | +| iOS | ✓ | + +## Install + +_This plugin requires a Rust version of at least **1.77.2**_ + +There are three general methods of installation that we can recommend. + +1. Use crates.io and npm (easiest, and requires you to trust that our publishing pipeline worked) +2. Pull sources directly from Github using git tags / revision hashes (most secure) +3. Git submodule install this repo in your tauri project and then use file protocol to ingest the source (most secure, but inconvenient to use) + +Install the Core plugin by adding the following to your `Cargo.toml` file: + +`src-tauri/Cargo.toml` + +```toml +[dependencies] +tauri-plugin-websocket = "2.0.0" +# alternatively with Git: +tauri-plugin-websocket = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v2" } +``` + +You can install the JavaScript Guest bindings using your preferred JavaScript package manager: + +```sh +pnpm add @tauri-apps/plugin-websocket +# or +npm add @tauri-apps/plugin-websocket +# or +yarn add @tauri-apps/plugin-websocket +``` + +## Usage + +First you need to register the core plugin with Tauri: + +`src-tauri/src/lib.rs` + +```rust +fn main() { + tauri::Builder::default() + .plugin(tauri_plugin_websocket::init()) + .run(tauri::generate_context!()) + .expect("error while running tauri application"); +} +``` + +Afterwards all the plugin's APIs are available through the JavaScript guest bindings: + +```javascript +import WebSocket from '@tauri-apps/plugin-websocket' + +const ws = await WebSocket.connect('wss://example.com') + +await ws.send('Hello World') + +await ws.disconnect() +``` + +## Contributing + +PRs accepted. Please make sure to read the Contributing Guide before making a pull request. + +## Partners + + + + + + + +
+ + CrabNebula + +
+ +For the complete list of sponsors please visit our [website](https://tauri.app#sponsors) and [Open Collective](https://opencollective.com/tauri). + +## License + +Code: (c) 2015 - Present - The Tauri Programme within The Commons Conservancy. + +MIT or MIT/Apache 2.0 where applicable. diff --git a/plugins/websocket/SECURITY.md b/plugins/websocket/SECURITY.md new file mode 100644 index 0000000..4f09bba --- /dev/null +++ b/plugins/websocket/SECURITY.md @@ -0,0 +1,23 @@ +# Security Policy + +**Do not report security vulnerabilities through public GitHub issues.** + +**Please use the [Private Vulnerability Disclosure](https://docs.github.com/en/code-security/security-advisories/guidance-on-reporting-and-writing-information-about-vulnerabilities/privately-reporting-a-security-vulnerability#privately-reporting-a-security-vulnerability) feature of GitHub.** + +Include as much of the following information: + +- Type of issue (e.g. improper input parsing, privilege escalation, etc.) +- The location of the affected source code (tag/branch/commit or direct URL) +- Any special configuration required to reproduce the issue +- The distribution affected or used to help us with reproduction of the issue +- Step-by-step instructions to reproduce the issue +- Ideally a reproduction repository +- Impact of the issue, including how an attacker might exploit the issue + +We prefer to receive reports in English. + +## Contact + +Please disclose a vulnerability or security relevant issue here: [https://github.com/tauri-apps/plugins-workspace/security/advisories/new](https://github.com/tauri-apps/plugins-workspace/security/advisories/new). + +Alternatively, you can also contact us by email via [security@tauri.app](mailto:security@tauri.app). diff --git a/plugins/websocket/api-iife.js b/plugins/websocket/api-iife.js new file mode 100644 index 0000000..d09cf06 --- /dev/null +++ b/plugins/websocket/api-iife.js @@ -0,0 +1 @@ +if("__TAURI__"in window){var __TAURI_PLUGIN_WEBSOCKET__=function(e){"use strict";class t{constructor(e,t){this.id=e,this.listeners=t}static async connect(n,s){const i=new Set,a=new e.Channel;return a.onmessage=e=>{i.forEach(t=>{t(e)})},s?.headers&&(s.headers=Array.from(new Headers(s.headers).entries())),await e.invoke("plugin:websocket|connect",{url:n,onMessage:a,config:s}).then(e=>new t(e,i))}addListener(e){return this.listeners.add(e),()=>{this.listeners.delete(e)}}async send(t){let n;if("string"==typeof t)n={type:"Text",data:t};else if("object"==typeof t&&"type"in t)n=t;else{if(!Array.isArray(t))throw new Error("invalid `message` type, expected a `{ type: string, data: any }` object, a string or a numeric array");n={type:"Binary",data:t}}await e.invoke("plugin:websocket|send",{id:this.id,message:n})}async disconnect(){await this.send({type:"Close",data:{code:1e3,reason:"Disconnected by client"}})}}return t}(window.__TAURI__.core);Object.defineProperty(window.__TAURI__,"websocket",{value:__TAURI_PLUGIN_WEBSOCKET__})} diff --git a/plugins/websocket/banner.png b/plugins/websocket/banner.png new file mode 100644 index 0000000000000000000000000000000000000000..6153c788cab6f46811c84573d22fd9f13b1b1b78 GIT binary patch literal 39670 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 literal 0 HcmV?d00001 diff --git a/plugins/websocket/build.rs b/plugins/websocket/build.rs new file mode 100644 index 0000000..deadb78 --- /dev/null +++ b/plugins/websocket/build.rs @@ -0,0 +1,11 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +const COMMANDS: &[&str] = &["connect", "send"]; + +fn main() { + tauri_plugin::Builder::new(COMMANDS) + .global_api_script_path("./api-iife.js") + .build(); +} diff --git a/plugins/websocket/examples/tauri-app/.gitignore b/plugins/websocket/examples/tauri-app/.gitignore new file mode 100644 index 0000000..e030fa6 --- /dev/null +++ b/plugins/websocket/examples/tauri-app/.gitignore @@ -0,0 +1,25 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist/** +!dist/.gitkeep +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/plugins/websocket/examples/tauri-app/index.html b/plugins/websocket/examples/tauri-app/index.html new file mode 100644 index 0000000..3f4c7d9 --- /dev/null +++ b/plugins/websocket/examples/tauri-app/index.html @@ -0,0 +1,20 @@ + + + + + + + WebSocket Plugin Example + + +
+
+ + + +
+
+
+ + + diff --git a/plugins/websocket/examples/tauri-app/package.json b/plugins/websocket/examples/tauri-app/package.json new file mode 100644 index 0000000..cc328b8 --- /dev/null +++ b/plugins/websocket/examples/tauri-app/package.json @@ -0,0 +1,19 @@ +{ + "name": "tauri-app", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc && vite build", + "preview": "vite preview" + }, + "devDependencies": { + "@tauri-apps/cli": "2.11.4", + "typescript": "^6.0.3", + "vite": "^8.0.16" + }, + "dependencies": { + "tauri-plugin-websocket-api": "link:..\\.." + } +} diff --git a/plugins/websocket/examples/tauri-app/public/vite.svg b/plugins/websocket/examples/tauri-app/public/vite.svg new file mode 100644 index 0000000..e7b8dfb --- /dev/null +++ b/plugins/websocket/examples/tauri-app/public/vite.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/plugins/websocket/examples/tauri-app/src-tauri/.gitignore b/plugins/websocket/examples/tauri-app/src-tauri/.gitignore new file mode 100644 index 0000000..6bc5d6f --- /dev/null +++ b/plugins/websocket/examples/tauri-app/src-tauri/.gitignore @@ -0,0 +1,9 @@ +# Generated by Cargo +# will have compiled files and executables +/target/ +WixTools + +/gen/schemas + +# These are backup files generated by rustfmt +**/*.rs.bk diff --git a/plugins/websocket/examples/tauri-app/src-tauri/Cargo.toml b/plugins/websocket/examples/tauri-app/src-tauri/Cargo.toml new file mode 100644 index 0000000..7c719eb --- /dev/null +++ b/plugins/websocket/examples/tauri-app/src-tauri/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "websocket-example" +version = "0.1.0" +description = "A Tauri App" +edition = "2021" + +[dependencies] +serde = { workspace = true } +serde_json = { workspace = true } +tauri = { workspace = true, features = ["wry", "common-controls-v6", "x11"] } +tokio = { version = "1", features = ["net"] } +futures-util = "0.3" +tauri-plugin-websocket = { path = "../../../" } +tokio-tungstenite = "0.29" + +[build-dependencies] +tauri-build = { workspace = true } diff --git a/plugins/websocket/examples/tauri-app/src-tauri/build.rs b/plugins/websocket/examples/tauri-app/src-tauri/build.rs new file mode 100644 index 0000000..b055ec3 --- /dev/null +++ b/plugins/websocket/examples/tauri-app/src-tauri/build.rs @@ -0,0 +1,7 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +fn main() { + tauri_build::build() +} diff --git a/plugins/websocket/examples/tauri-app/src-tauri/icons/128x128.png b/plugins/websocket/examples/tauri-app/src-tauri/icons/128x128.png new file mode 100644 index 0000000000000000000000000000000000000000..f8d9962cc21930b871430f46b7dca842cb6c6f6d GIT binary patch literal 15920 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 F{||SSwqO7N literal 0 HcmV?d00001 diff --git a/plugins/websocket/examples/tauri-app/src-tauri/icons/128x128@2x.png b/plugins/websocket/examples/tauri-app/src-tauri/icons/128x128@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..44d26d04b6f60f5cc6ead44f15d30f80fd348b28 GIT binary patch literal 37546 zcmaf4`9DXCp&*t{EZhk+qQj#!|fAfRgO}#j+ z4+pe9ygGOA!_fZxeIkEECce%lr^s)gi=(wP@k7r{;PQ+f7wyOOUA>Ob zq?hs?dqK{yklbA0dH`Q?K^JFb>J^_eDt;ZD;~UI`Bf>m8wX+vbo;{f(6{w7(d+inS zg6-Q&s%+yCDvd(<6z}dKl@x#=VoRL~6`K`Vid#>{BnCc`)<)mI(#w2F%T}LfO0M(A zx6GNAFhwo*q|K8ni-H&`nS1q=8<@Yv`=nXRn*jKYPb9W=^<2YHdmPf! z|GzV6>K(Xw65&RMX7&u)LIgq3%t@q}caXC!)u#B=wS6x;5OyTgRzl|9wE?n_JKxTW zfTU}(vo${=EVv%{KIo)H(Dqqov0a+)yO+t)$|374*QBMY-?{~R;j43x;OyLvRkgnc1+!HKulKZj&8jO${RKNS<4KmNv*1iKx=bA6@X&vDzSRR4i5d-GXN=$hV+wJKX(=hIzaBT&n!)? za(FFj(|`Ej@t95`ZJ3=jA%(xzUKb3XZ0OZvK7Nj9EV0L^XYk&9SS9`jaS7|jcA}RP z{I^zhvwE_3be^3*+^4`4wLQ{`AgNrJ-y)OnDADWu4uz< zXN&hoAOKtZss+EeB8g3&>Wn09qdBuD<3mcF{Sew$q!PE*mM6AcGEl`nxVJM{?n|fW zO=*Tx5%ijX1+<9?kNL76?Z~E-9 z<3`g09jxt(SXC zfSfpduV(4PEE7&-6imN>TP4#k_;zv0iI%|Su!9G7$KL58CBIN^{Fgu6DNP8n zz-D7Xzrv$SxUDKLaF0t884%2#*jjZI9*oAU&1hqY|4udiBe{)N-}MS{Y~XSGI+&&m zKpbFm3_y_~x(H)TUSo}7D)4?6^YL55{sd3F6B@LI)GKb3#BDKd6-XKFAWZ&};RTco zJMtqdR?GR+O+63aoF=tPm#Kl{&DuqFKqO7X7h@2DA!;_SWlZn>SNajcc*BUfcbZ|AI9BFbz9 zXUI@20>fI%C0~;6hM>v$21rl*z^?O+qC*~ihmhZpNc5X{-NFF;T`Fm>WG-)WmirvxmG@lH|YY>wk;;u$3B0K1q!uA9#acYA>7P%2Ja z0my3e)H~nE6kbI8_$bwT;`Fq~lRJiTv3;cWbXA&oAx9-7PzL~PH`c{~18zSa9Rx?d z{zMlr;5^lRBROoqaNS9w4rLX&zW;3?Chv-PGg6Yw)#cLDICgoDt+Dy%{zm<|m@rg3 zc~8=T?mY?Y9Z|7mIO>zFE*kTy(&PNDx!t{s5U!PO*+2I>f_Bvcg?11;pc0lRG^%&n ze(NGc2_V5WGhCRDH-x$~UXX@u&85Mle~~H7pQ;E!u033t6(vgYB{Vu$rCR&15O-Np zHU|lafGyq&H&CP*W89;kn2~t~jDl|mm*&Xzrzsf>kItV4m+v&Sp>erp3#_8%rV_m2 zJInr9uIeTLt0kAT$N5a%p#wMl2vc~NE*^$p)dup_93hW9_N#ev^X!?wjwVv$+VBB* zXgPvS+Bi5i5Z4_nFgXJIIh5+m_onB+b0()J>d~7V6<58N#3-j)Vj-GA$1Tnsffhn2 zerqVI4j?O7AORIkJVgXEGK4@3VPh9i$GUEcrp)|q+%QU^9#0RhMuDOMNAeBAP}z@u z1{?NYecs`|hyB8zdhN80TaTEE?d6-9-EBxNesfxt_nmE(5kmH(?a$IDdbB4v7Yqe-ancQ<@ftaoE4=8yARrC%DDRg>lY3)sWSQ8n3yV zL<#8ZtbP1TB?Ly~Iv*=V&R`(SB`oQ#*Q_fF*$B}G(9PCjLBhqF09Q{kKI0Yy4!9I8zgpnmgL z=*rLRx*)seeqeRkENYM0qaDhXDC~AUm)=~)aWqpa!#H@h84=}P&!+c;w^ls%^+gnK zPlc(`j)8AE)sbT-I)=<_tkz_R#CfRc$s3+*{E6V|8xbTg_Pv zi9}u{Q24(_qdbIX9~t3lr{hsfcM?Ht{j5H@ta-x!{AiAmMMH@_EwLo*ym(v-ZF{Sz z)dUIuGMYbJnhgC`wWwPmu7zQJX%fZ%G@u-r%8rO-l$DV231Q6rv5*Hjb^*8>if;_A zSDR1dGS~d$x`#cDEcWiRvicWamInTV}XZXfg+jL|9PJLbm+DEVscKgLar~jk)zOey_$Py2q38p zO~0m`Hsb!JBc0Myl^%4FZLQ4OFOD-+kG-R696pk*ce8@`Yk_4%z*n{%to2UxXa}#T za1N32%bLAzP7-83@l=5!0uLq^ff(P1ED(D;k1|}HSnS3(S9{IsOLs`S&%PGF{M29C zk`otMq~sJh^4|D#Y`d;7axGIU_P!~ao6dwQADFTIlPZ1W z=5NTA{UwjeK(4pgAc~c=t$K$V&M89tljfQB?W$#Za_LK9&|!5G=F;CU%MA3C%_$ke zEoxxiu7?-movgWXNiY^;F=u~I;}IGZ0szTz=B;9B*HM|vbUnP^iixXBsP}OgG9K^t z;1V&zth?Ik)SMRfyP{8?>Fy?d&wCo3NXZlTbLIzU?MY#6fDC)UgOv7TrFLO8!8`Fg6FGIN%}&D@kck?qE#aY}BPaor`ES6fi!< zTuIIH*+3$hZ2Vhlkwg*gr$a*voDD}Yas8-EReB~#w}gJbD?^g4$2Ums)zVJOnH^*A z_)Zh_n~n*7_8%Ju+qn=MciWFYn;4_7Jor~t%M88GI*ETS@ulv$-z`_$zhgT_vAEfC z0C66|^*^!Hz}#(s3g9C(l!*gcXamgN?`7t{Gw;>Qe1*K`B{rRor6g7%kUvuiDmd+Y z9zh#7oRYvz-{jZsWo-5VCecr?r9_AqTG^`Od5{3isyl3cXL;2y>z{vL*tDW!GIY&Y z5IHgX4(DXajogPBKoUFxaH-1iFU8I2hU;9L%(%+q7#`@{3@XBx|?2u(|#F;4eSiLG>28Pz5cJ{UV zFYz?CH_Z(cUK58q-;k|OhofbjB4m1RHO`~FFs?{M8moDdO~h=eK>6`kxH0>a zX;T*Bg2*CYZj*R-&71bNI$*U~%9IRkIGu`$st;b4bkg~6C#8O89k1zb4s zh*$E3Vn;AU9WQIRl1A;X^V~l2=nlu;UH;ftSC|L2x9AgO`o!3$i-`Ynq?$J9bpSU! zeCs%H9#1@n$Dv3LT-ultBsnG_XM`4IOlETZiSS;!PciD@+ae|B<1->q&f0eBIKRy7 zi5uzH*CTxff?OVl@PlH=wflEbcI#PhFVQL&z)KiyI(=fq3ZQsGH8~u6&m2HA->KPQ>V>swvT0KK7&`&zu&Pn?~yt7au&d%)qYZ%0jtY7$VThQ3SH(; z6Yrw%GVpN15xnG%7}9M7%L4dJ5ztz6?9AlQPv4MPq03yDmX7ug-aSVir0>VtxGv+g zoqNAbb6cp$7+xeV81e(ppGJLM&P}psEiCp21%+tWX%L?)GzsAZQoA?AY?rLUP<*UD zufYh&NQZfnuql|<{#!C(Xt6`4E7wCfmNn0KIWL|*J=^5C`rWvH>}7_+>;O8f5b1em zifE4+c%gF7PW(`hza?M%N&IRi$Bw%6`RbyeeNK*BI6r+o+RMZTCJthhwGUOTOtmG; z)zGd;h*41B1acra0S(IvgExirD+w0!Y~l%v?6^q256XBXxO0rz@4(It+N~ykS#PEE zuLoVrH>b~)M@d8;%EAW>;#OrUQ1+{V>A%ZXkn_o_?Y?B@c-zR|_R$KBKGKhM6BH%; zdLS0b>P*hy)%{5@-fZ-tt82OL-`q)p0au3ZU%KGsJ+7qeG5G3|IbEOhMHO7H?wpoN zFj>i~kcsqA;ic?IXQ3hylxN7)tLmtNO?_fGeHv?%f zjF&>Xa{~?HwdbZM2I}_>*%;0yydyKKr1rS>L?415A?R~xFV|eh5x417>gO+NVFWsa zH|x=ro!)!WJu4mF2bc;z%9dsR+ht>2Gi&u-M}5sDrrxl!8Z-L@)&nK!=(Z zTLnJOuBSep=IOB5)KjNQETHZPrBD{a^GM(8jHHkk@fq$XyX*XYw4#y82RA?6vft>r z^pPEE1>sk5D;r$*+PMDgiNna==WWLzm_UgP1#rA9>?ZcV>^p@58CU9tk^4=zOan1@ zt3GsZ%-ETp@PJJQl>@SLjyLH~51h6Tco^5`jRZ&h%_ljBiV4pTd{#gjL9KU;eaYSqqF9{7=O`xQG^%(?DE?&V{&ye0F>)? z5;sCF9H6Jd%}70++H;S?0#{f)NBzSXG6|O6*^~jxW~#bp7VK^O3D*o7Jn*<1RUcY? z#RrWTW1gn?5bDi%>T&z_vzEz)W(-w|)EGvZJIn*3fyqZbyvqtjcAGY{Z8<3jsCtS( zFJt4gUFiuSK76&kvd+^xIDBS*`}lhtJ%YO^WXhvmR$iC`oX$j^_}B15A2VC^Y367^ z7_u~37KLcN_^H2*t2udX&oc#pNr*mGsAqd+w*6|jI-`BU6sZB`Ld2KsfCfgt8jv;Z z5m+BLUs?ZJ`r+^eA)hC#eW(EIG6x;#3-ap12X#B5ZcBtec08=hUZ~st?b*zi1|N%P zx%kI(`?up9^S`FAT>{41^{CZ^EH3p9Q=-vB>;Wp?_AqWsISoc5z5*o8M$#iif$sot z&OkbT_t&R47KuB;43l&?oJZp{aUQ|tmZ<|cm26i}$>8!eoR-b@w+6rP!msCkd*~&D(L9k*j^*hTj@Hy3EsO|q+6*rRI=B&!y#vtH>}lV-iWXkBi$;$0rhle_ zVTDvPEJB~eEWd37LuWv5Srjr{kQeFuj#|=L$MNBU7K8l-ZWcvPy3`+&- zVj!`DQURbV+MqQa$BSg$&||+K=ar3w6G`t9cFiq9J30Io+n~TnW|cUDtNemv}9-^;6hYz z1~=Gs8c4sXmh{R4n!hZ#JI8s3=U8mGlif8qPT&h<(v~M%%SkLpp^<1 z7AEa+d4zgGCBjHULD$?Zcek%MS1*FzF%Z-!LwkKV*zd#1b8GjLk0uRj#os@+4KzD` zpKa)PCP3Vz^br0j24Vnp7N3wyg6yQ5S#$~7a4Tj#`Bc)ozk7rthjrBhf82Ld;cgzI zCq1W+6jOxcZqxb>q`{u@V8Ba+do-TK9TzRA#){S{>N1w<8{+0tB!yWk_jWR|WR9mO zXCweb90wRch6_o0I>Q>!Aa5zJC~iwF%p9if*V}qiUQGDp9snrYI^PuX!}+OXM1U9G z0jk6+5dI6>7K;{{7|VQsxUD<9fd1>G3glevHT}xMFjyOdK6ahe)5MvJ09@1mJ`0r8 zvt}SHU8-ZYrxS7;yIEuYD7X;gy|18AS22m5ta4C!gJNFi}R zIpmu(3zTeX3V}`n@c}#1?bx?aV$!u|7uI=u$p@s7#rJl5K%ftR*E{0xU0~!?xd1>J z?{feMzdhf@M$%Nzux_c1peJ{2u1MS^dce#nT_<@Wo^^7XZv9sg#5f3wN`Z*Pd*?gV zKJ2~*?oN0Fa3}p!{!Ux;LEP4`;5R;N?QKl}fcTRt81C?RG?c4})?#t8yq|i8brcki zUP%PB$6R1k^u?p^*&@@G<9RBnXD*_$WGRLa#i-NF%WmLA1yY6ly%C-CGQA_T53Zdn zrrbP-1eAF}+)kE6hU5C;XIEI zrT_sFU}dqjF#o+HlS*@P4hF#3uF{2ng-yLs}K zzZ_6oCU_WeUVxW;SJp{0s@Zvv@9E%QX~0#)J!rbgaU5|Vg2rxtDChFR(Of+VKtw22 z01uVu3t*r9K&p$1i8uK;7&atOn?_z&1WHp zbEK1E}x&fNAa~JQ{crPM{<4w%c#Ewe%fqIJI?V5TPGRvwGJ<3V+9h961+10bnLC8JMMAMUiV zy109OJMP*oa+1cX*Q<+hA6f#6?-*UP)nLpV90g#qb?#Y#6quEV;cxl6a{*G2G0=Woa$;J!% zS;_&`;sN}<00MLv>A*T(y9*-C*^z(F>~98`M-pLivI!+GUz-@WaF8a-iWn8sgY&az zWidePU8|Zzy6w5OF<<2EMbaz8cGzSMW;ddiYw<)U3%OEEaXlLl_u;@}aZu>?^#iw! z7u)St)9uCR?X|q|VzsZZM~8Crnoc{aJ(q*2uZOx$jN@zl8=p!K`v7=FSd-1NeLUE0 zg$?*)HLG_h+2YhJS`c3`p=E|ZY5>i697f^Tm(@oDoZ)HNc`H4lQf8cCr;^)q=e5Tt zthwy2Ltk5pFFXwRR!N%s8)99g(D}YOtNeSuxF=6{*z@keq3|)?(9ll#iB-bgY`sD6 zWQn0{ouu03D#74&xVGeTaeqrR0Eu%sI}1D&N7@czmHG9;_$moUS+ZnVlAGQ$G&Bs* z^Ic5RXzn69_AoZ!XmR>4wf#9lEv6h;Mn4D?Ud6 z6Rlkeg^K1CUfW_FBhC?`17chJnwCpnn&<3zpi?3ehb;ACTCUZ&fBaEf7j~X-O}&`% zCf}p)xcwU#5&~9q}mYonO-b~pzZikBOC?`&9MPdL29FX(GIBN<|?A<%Ih%OqMGY{`lKKOWJ zP2Tg%K+8?OyO%uKcgmiKKBZ4T5qH4o(h^aI7A{^gGo+A1GO#bQOG<8snH z)c0B@BrS^060+fORa;VF z7HXwsA&BOS=u9JAh5_=^Q|YaTD8SU#)Slf*`{N4-es(AI*J-(BNY-6@0dyVR6W{*+ zYFhpL`$gtkUM&>As;{-$(3tlZ3NNJW=S<{d%0m?!q1siqzdvmb z$j|=qin#>5E4cvTaF)zn%#Qp(Dep`92l&}HSt!2@Tuyu7Z8X5BpJWYX0nDgi*~iUe-@y`pWWtSBgWDD

j#(n6`cTK z7WaNlSbCU2SYaJ~J9!EIl=Absd61in`6c7 z2!rKnZY-bA9QxOd{4Z=S0_3y53^x15sxx56rA9s3^eZ7eRU*!T!jS;brox|@Dmbs@ z)LV{9g3&pS@bdnpPmjzHE^TUkmTaE`4_*q=x`t*bmC67J>8E`rU!L>bFU8ItpIBK1 zt(tU#bBZILUVIruUJQMfM)-V%6~kN+00(_J2iOS{_jN4?CDqbi{V#5oC*4Ics&)CL zfXG;fbi@h`QbCfpy~%g39f;ohc>dZaaPZ>!3@VVdYc9woE=YPAJ!M1bIamkPi`ic2 z=3^)V0L54|3FQZKFz=&B6nu}0Bs6*^vk6umxWDbO?2NMj(q%JH0LEv(0$S2O6<(Rz6q_TTo5=jq3I z1^Mj{;w{@ZApHzNSjiXISH&A%b}i6rLUlvlSK#hjMIJ(KX5fOCMyc+NjS2rMP&OL-kT@XW&D|OMh!+FR^9qfpeKhJXtBu<}k_Gqld1UUogeKE` zMNL%XNxW3J0P<^fy8N{}el#M$1(>4P(D;IPd4JZT(mR9GkKtg9jtR5eH#&zS>8#@GR%W3)WdwTfIV-;V;gDS{Wri zspuIA1wsTXFH_( z^OMtaCG-tXX4EI@`fjlW{)p5*!^a^KWusC6=q{RcQgzY2{k$FNW>E+dwP*Pys-WNR z6yR{%y{MhDi=Jg5%f9QCS!_f0Yf-w9P!Dd0?>-@4wJihJ#I~jE;qqP_MzHmK+(7sA zwABxmOVYf34Xd~249uYFxHjhju)>@|azjc=ClaXa6eAn=J9PFRrFm{-I?wqXU`5On zM)uAw9hq(e+uETgY0ABb{p^n0*~&axWAA${`(xx?AlD-*o(f*v?Nv=4()iAk$xB<& zrGeKkHs8+`l2B6Fw@X%UFL@dg!j4J`?Yjg9!OxGw6932*KgR$RFIKB?y;+#o)7n{g zL^s_2;w`&d<$Q&T>nXf_&fo=-&9{>T+cnrN-hPDU`etnykC%_k(5JUZn&WpH-wKP|-=6QPL;+I;sbYSMWotrw>4(9M zCqA>k@Ijgf%85^!9Vb>XJ<3Dozp2&xe)V|AWVF(`W#V&&C){=&>$*ENLjP(;M zlSg7yCf;ZXaYW>`l-aA`{Z{NG|AlIEKHE$kpK-!`U}WJG9WS0L4puU_~`;D?Fealy0X-<}1EccbywkMOL)WgdPk zW{%$HX9x@R8M%l245B&V|9>qq6X8oql;2WYlA+i}_q)chCkFQ%vx|ys&)b&cyL?oX zHOaFChr{XAKiPT7r;mp8-(F#E1xVu*>36zEio1&Ya#z;020=SH{&T?hz&ENy0=?IC zRQ;V??ZuC#L?%U$voY!P}DX_)CCsy6-b}4 zR~ucq*?0P^6auWd$Yp1Yonm{d`Ll+~MHLr1vvB(H?BvN?-kEq#UdDv8HiqG!p7H{< zzHR2?4}KLqeU57~#q|(n9i~*U!Y{fvc`CfkF?ix*d#jr})6^S21S*Lr9QlBxJ$6jk z%;@Fwc(-mBd^Wj=Bf_bJP3@r}`iYK3NpB7qYgd2nvc;auHJVG#EV>?gk!#XL|Ej|- z07z9Tt(#3dy-t5$WUQq|=_R&xeLQlni!nvaeb)`eWN_>>g}zSWc#q)XV~)8g7rdG| zg83mDXtCS70;cKWO6wsK`}vyD;f#_cgIq#|B-(gT6O1V4JKgxM`umJv`(rR27TT0) zh@VUAhHL5axx$8ZfKxKRo1#hI*I3ikE`jNzIykeVSq*3O+`5E#%*Yu@$R@bo%^yCgP4dx$?N+N$<4V>l;qZ z43gkjD*&hAhBg*`ZBD?%BumAf5_9p3DZLVUg6cl=FIEi}+X(E4!9Pa+f%`dle>~kY zO{!`axsp*(!TnYQCMUGk`GZ@i98|1`&pP9$8EXTY@~@&Tmwu^1HLoQ_y<%d5R5^B! z@x5Z%!w@_A5~Y}g93v4n{i}5JHW5B$8JD>KMF<0A2!2h*S4P1cL-|5s79sApQ=?<^ z2_J&cSe~=Bb`3dfkteP*MZ`LTO**#cs#HYWeWrbe=xGbcvkKgA?PQR@8G{#5f{(Qv zMjF>>(DZc?+5mV)JI>ZdvR_!o>KBgb8^nqp+tF%Oaeaf?k3s?2sl>=B{dTcy{dO%n z+YFEklA?*ee5&j+e{n=vYFI-#>er3qG?|lzUj13oAi0<&C?4U?iC>Ab2R@9}dbn#B zPR4qHT1qcF{B;|f9p&`CN_}m?A9^}Tg||9lm$}|od@U(<3anzmgEvd^v-bnYHMBw} z3xsxhZ_^lmHj9kdM!peie2B{lKV82~Np~V^cuMk4{0Pu_8aQzY5)GJ%bMXCql%rPL z$&a8mKD{_KiJd7~-OoKId)Pgz(i837wS_ z`{8iqe8xSnz?yQyownR}zqj8)8I&(}9>J`x{3!hQ+_8O9!5yRt$8+Z^bUvT{uUa<$ z^ZK`W`pLB5P5~EbCk>Pk1sHjA1jzcG@{U$J^=BeWPEA4qPlUsijHARYEm1%lw_zdF z;M@~*?a#0b;CYqFQLExNU~+CDsvnJyA6Pvck{ zNGUrs%yzKoZErT;eOh2Yf!2vuQ9O%frbH(F*t;A!R+p#jbq!m)@&l6Rt-k*is=Q4` zS9!(EF1B1{haJ=$kQAIaJ1Xa^Z>u~AG%=zs_!B!b|8_IFgGdX*QQVl^m?*NA#g<9* zM|Y(I)Sx>t?w`LyI-jyoV)`YX8_Q*yW6YtIkW}@^J9m&<8!<@^Hqq8YN{&#aD0-NN zpdr@Unnh5E%YV$u`M!yK=GK;)hkiAIfnutT13!S&hgIF@OEl2oSri-}6&?Ka`d6@4 zSOQ7j-#Y}r07ZDhQiRYlOo3#ZFxVugoaiw1g$ z&OAB$mV5a;3-WI9l?rDeYD`p$O?bF2g%W6OO=|se`WM<%RT-m_M~!6&b8v)!J=h#u z>r^}2M$#d;?KV6%a+`_rE?Y|bTR60Xyj&x0B$ugu@1dTp3H|m>`;YUiH>l}qhukEE zOzE5^-3jPB^b)?PKaB~Xg+9RlmGH8EplsB9UdJZPk^6Y2Sdp^x6Dv1RAx;g-3A&At zVhSBDDd^9<$3(Q(F8SG?J4?$>ch>XWmZhnx^hUVQRVYE4LB)tcTvByu^ki(j8%wwH_j+;xK3Tq1hw0-v(ji}QIN zJAnJ>vXml|)tB7`BTuJy&riROoq=Po7y;MZO#^Hy7e56kWA8tTA6B_w#gTG2SfPDH zFJ-do1#dIb#BLP@Ph<#b>10r_Zk`%Ti{Yr}XpR)^A;PW}F7W|#Q;AnGmz$sf%W8|n zm9|R($TfuSn~(+siOtkkN`VNsD{UWchi*?uDGtNvs)~vGA(}fml;TcdyU#i~va~mW z==4u1InZ5nSMvzLh(-WutdxfQMQj%ZJ^)($0kvDq5Khw!E*!y|zcak&A@}NPf>f4T3Y<8(77d#ZM7$|={ zd@3a7!7_zsc7zY>7=Mv=uXehX5CT6L~llK;U@;vrNLcHf};69z)cf zhBbpskIggkh=PB<>y^3I?I9<+;~E#U#-^0^8Tau9mtt7?=2He;gc2 z-iUtfpki+ZcYq=9ZzKswC!>2-akmS$9gF@6$n7L%iY{yv$!YOwD+Q3S)qGz{Cb)9QsmfSqDO#cDHSz51U|*<(h*JmG!ZJECrA@+j}XAOYyUtLxO#MDHi{UX+ST{ zwh4Q!ghgp_L0W{!w=16gyAOTS{hk{6IupSG4Z5+g|JWw%n@{iFYd9yu#H9QNiJVEq zfi?)Z>9PiP_XWpIgD2g}@-^VF*p+!pY+Yt8dtt`>WVW#Gqn*-2PiyHFA42+%4;sON z7-qz35-6GdFQ2s;phwZNe-fk3lWPL9;Z*N%qabB6yiPe^7gc)d6~s~>W9$251{R}R z6{EKKU$ieepBr}2eV=Y_=nv?aN)m%#zW={&;u6NViOWE1@>>g>UkBRm1M}vpkqzDg zxv*5^9CZP`Fs&)i!pwe?_cDLT(;*q*qvmgR7S~ZfE@z_XO>{f2vVM>rJtAG{5-LIu zZhj%6z82u<5!ij)yFdu)WNLN6NDyOipa4vrTA%%{Kpd-^8E?zWyMEh`-I{jfW>vsK z0_-WN0oPL+G_2any+F>#;o#>$M-vf^1MkkZ!@H3$Sut}Pv+?RaB-1m@X1n6Q^4&!)Kop1iG z^EdAZ0|RH-0Vmh*uK5UMFci=L24Q;n9#9;o&?jnEA@W!KkpsT>K9@Ni{dT)f0Avb~ zAU$_^Ubrhokn`;%9X1L}z_d3QXs^vt``MgWFgPZvM4e*pui6S4=HVkS$iPkg(# zhRK-pGi|prP2|h-bj_I;UA$LRE&shC) z*9Nc4?XUA7Fh+!N=k=9vo4~=A6dcWGtZGY06YeIO>F#ID7*f#5(6{@&evB+^dte+X+S})l`Ps2zIBRIhT`_?L6T6rkq&V=@U z00}xQ;7P&Gt%N4SgW4&$tBT37FF&_`&+yj;6O-YtCI+??jJRE$RmnYw+ePR0>|$_< zRRw$oY9hGqGti|}?(}+J*g^=Q4gmln$LY7MBRGyP^ap%5R5zWBjn138MiZnf#W6)- z1~n&efjQ%%i?NI2{aHc0m!U<*&d--*H(#_ejR(Rgjxh!uAZ3OqB7vMq*UfK$P(G3r zfWZ{v+M0xRQo4xp1rfFe71XPjdi#7&N5R$37^L}&4oEXKA)J(f5(4!5g!R`uKHD=B z!5@_U=aEkRq-_iqaivq_A$)MqfZn;)+VpjC&#Pr~$v(;3b{nvW{ZgU1lXy0=4z`AP zHF$6g8fQSXB4>TXiea3Js;9>nCMB*9P^6&SYAkOa!EbRIu$!{Zw||WntUV)%=RsYuaGq5BBaKXKZKkr9A1n86;IMozQLJl=QV zQLV}v_icQItVf#<3c4-+b3qLES;}ItGUO^qJ#Y?P9BtM3XZL7mHD_7C$td$lqd8g#7G3FJ9 zC?q=p!M8)^M9;B!N--N!nTXKy=w@Lb$Nu-r(uSkXd>j`RWGJ;|0Hm!%H3>G#>B$C>~5*erFo>G(O4uWx&RGUy|| z`55deGqb2L*JW+`bh<0MzQR?LM{9^wf-Hle#-jU+S(-R# z$$9wjzJioWx!J~Owtb@Cq|f5ANS98o9LFmC+%wD<1W4F*M}z{52WjPemI%eXiT^@T z896x7iuq!|3Y&==8M0hd23{&cV2a%1FH+ty5XHdS{5$&T)Ss^D@|Ilt+UE?NnM*MO zXXTsRoY@Ohc1ZmvXh@r9euy45CI^ea@mW}qWvSBGFh%VMO{%I8{? zr>ZKfo3*gWZci2>oVAPG!L+%LlyP&J1D|lloZ{t|#xNcO0g99O5WLxVa0Xsv$O*9h z4zb3`6lRwBGm%li-0o}a_)-8V%AWDuVR+!MB01CpHUmmnT#&oFlY*|`Orzoa)dobx z+9^4JjpL_9TAni2kyQ253z2XTfX>Vz3IM?A*g%(p6)uVwR!iYGL($IP;Ch=GP*%=+ z!?Jha`#pPF9pVB+lhv|iil`R1!-{x?aWNqdv|o>?c=n$8w;Nz#{&v6@+tBU>`u5kr zZ2bEHf~i}FB_e?R_J{IBNa8nwFPv;uxE(Q$#{AwMPrnGRU5tb36V^x_k^;EIRAyMb z@kmHQ2(yQA>8}~hEAMmI2PA3tK|SOSXMR^%jwdLPWO{!qyB?X)s1g!|#5~08U}lKf zX+6+wTB?Gd_fBB3MEBbgS8eZaZO|a;*7&Ojfs>P1Bc;GB_EVzsYl2c}Su8~;bizOo zLQDA(W@)5hM&3o!Q?G}^!H(wurBP$A)IE_oY$*3@;1VD731kQbH`!nT*`JtGIGpx# zGf&-wlKDC3qmd+;8#ceZmthyZkoVh+{610#JD`Yhbio*zN5;#2!Q0_PM>5a zD*azgWs;|CVg+_&SPm}FRb;ZsLu&fA|9eG`V0fuSr3MUDHNtQ0$Z>~Qp)v}1b$wLJ z1j65;FifqOH;IJb!|*q7s044WRL4~NzOCn2;!|(Y$=lesLbyI@g~%Gub{rEv4mSRE z$T>aGws~al?gO=RD!*4|iS-5o$tn*n|14Lkl&ebm(T{i9*E73%7W)_WmEs)veX!u$; z<)56YiMGDk_*{?6v@Trp7WTj9n=%zCZx8;1Q7si>h6OM*(Y}4+z%Z^2fvLdpBz_BPWJa=X@t2RoRYHx8UDc(FMDes0#t{m zJN~UZ$e5hu)m+P0YCvsy#gNyUH=C`7IXXc&lq<( zO=8fzy=*Cq(L{Mx-sr04QO1bafq?WCbYv*Nlnqbb7jbK3#5TOO&jZRZt`;q=3l_Kf zwbFyo9ag`ZTaEe*d1r-qtj4l_+xE^?v|B}lH@%#hmQ%17FXroFh^odA;zkaY%b+cr z1vBbDoXxt_A`j@Zn@HrUimr|C*wO@|IiAYrs(9b;?+{Q)7N{&IT?EEHwYeZCK0EfAr>{ zSRY-~R@`MDWMVA+;iNa-&W+>Y#$o&c!?x}-bSxEyh4U4SP=i5ny@cCOJQpekPUBr) z%`_)A332nP6s;}efR~4!0G;Xu3VUqyjuPl zBj#Z;-t*TJfm@)F)3;ogmTr@EM)&o)4C0lYKD<`V?}Az6Qlbw7v$n=QuK>--&qz=J zCW{L4GdjWuURi9z`*yad_4?oR`D2>DOx(^+x}x-o#wm6rKePc3R)_suk) zNa)2_=Fl*eNDkTBb3(dy^J=4CNadCJaec>Yy{S*h&4#`3^!i>VO7o`YG|OL=ft?KZ zZDFt_IvPWK%sCztP#l&eCZdU2tx2g)L6c>?=e|^|)VIv&nm6ehc1VL3Dd`o#J8MTm zahs$0vQ!{j-ril&)?TpOW=m8o32$HdQOX2Dt2J+1`qJ*Sz}-YGSO1C7H*j8`I7qv` z-Z#NVZ~k7(&o=lg*jNtqR^G}kt5mjL*sbWYE3WhW-D~ApZv1)e5FTE0{^~O;z^(lO zb{Z9dBQ{KRPl8Y)yHs?}w@C&XDfb08K@=4NK!*l|P#4AM%#yNe#3~|-%b|mkMfjR1seCoi?DE5+n0dikSY)(~K(n8h zfDn%VdVBj { + if let Err(e) = update.download_and_install(|_, _| {}, || {}).await { + println!("{e}"); + std::process::exit(1); + } + std::process::exit(0); + } + Ok(None) => { + std::process::exit(2); + } + Err(e) => { + println!("{e}"); + std::process::exit(1); + } + } + }); + Ok(()) + }) + .run(context) + .expect("error while running tauri application"); +} diff --git a/plugins/updater/tests/app-updater/tauri.conf.json b/plugins/updater/tests/app-updater/tauri.conf.json new file mode 100644 index 0000000..705a73f --- /dev/null +++ b/plugins/updater/tests/app-updater/tauri.conf.json @@ -0,0 +1,33 @@ +{ + "identifier": "com.tauri.updater", + "plugins": { + "updater": { + "dangerousInsecureTransportProtocol": true, + "endpoints": ["http://localhost:3007"], + "pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IEUwNDRGMjkwRjg2MDhCRDAKUldUUWkyRDRrUEpFNEQ4SmdwcU5PaXl6R2ZRUUNvUnhIaVkwVUltV0NMaEx6VTkrWVhpT0ZqeEEK", + "windows": { + "installMode": "quiet" + } + } + }, + "bundle": { + "active": true, + "targets": "all", + "createUpdaterArtifacts": true, + "icon": [ + "icons/32x32.png", + "icons/128x128.png", + "icons/128x128@2x.png", + "icons/icon.icns", + "icons/icon.ico" + ], + "windows": { + "webviewInstallMode": { + "type": "skip" + }, + "nsis": { + "compression": "none" + } + } + } +} diff --git a/plugins/updater/tests/app-updater/tests/update.rs b/plugins/updater/tests/app-updater/tests/update.rs new file mode 100644 index 0000000..20099d3 --- /dev/null +++ b/plugins/updater/tests/app-updater/tests/update.rs @@ -0,0 +1,1101 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +#![allow(dead_code, unused_imports)] + +use std::{ + collections::HashMap, + fs::File, + path::{Path, PathBuf}, + process::{Command, Stdio}, + sync::{Arc, Mutex}, +}; + +use serde::Serialize; +use tauri::utils::config::{Updater, V1Compatible}; + +/// Every test here drives `cargo tauri build` against the same crate and target directory, and +/// binds a fixed port, so they cannot overlap. +static BUILD_LOCK: Mutex<()> = Mutex::new(()); + +const UPDATER_PRIVATE_KEY: &str = "dW50cnVzdGVkIGNvbW1lbnQ6IHJzaWduIGVuY3J5cHRlZCBzZWNyZXQga2V5ClJXUlRZMEl5TlFOMFpXYzJFOUdjeHJEVXY4WE1TMUxGNDJVUjNrMmk1WlR3UVJVUWwva0FBQkFBQUFBQUFBQUFBQUlBQUFBQUpVK3ZkM3R3eWhyN3hiUXhQb2hvWFVzUW9FbEs3NlNWYjVkK1F2VGFRU1FEaGxuRUtlell5U0gxYS9DbVRrS0YyZVJGblhjeXJibmpZeGJjS0ZKSUYwYndYc2FCNXpHalM3MHcrODMwN3kwUG9SOWpFNVhCSUd6L0E4TGRUT096TEtLR1JwT1JEVFU9Cg=="; +const UPDATED_EXIT_CODE: i32 = 0; +const ERROR_EXIT_CODE: i32 = 1; +const UP_TO_DATE_EXIT_CODE: i32 = 2; + +/// The port the endpoint in the app's `tauri.conf.json` points at. +const UPDATE_PORT: u16 = 3007; + +/// The server a test serves its update manifest and bundle from, shut down when it goes out of +/// scope. +/// +/// Every test binds the same port and `BUILD_LOCK` keeps them from overlapping, so each one has to +/// leave the port free. Shutting the server down by hand at the end of the test does not: a panic +/// anywhere above it leaves the server listening, and because `localhost` resolves to both loopback +/// addresses the next test binds the other one instead of failing. It then answers with the +/// previous test's manifest and serves the previous test's bundle, so one broken test fails the +/// ones after it with an error about the wrong package. +struct UpdaterServer { + server: Arc, + requests: Option>, +} + +impl UpdaterServer { + /// Serves `localhost:port`, answering every request with `handler`. + fn spawn(port: u16, handler: impl Fn(tiny_http::Request) + Send + 'static) -> Self { + let server = Arc::new( + tiny_http::Server::http(format!("localhost:{port}")) + .unwrap_or_else(|e| panic!("failed to start updater server on {port}: {e}")), + ); + + let server_ = server.clone(); + Self { + requests: Some(std::thread::spawn(move || { + for request in server_.incoming_requests() { + handler(request); + } + })), + server, + } + } +} + +impl Drop for UpdaterServer { + fn drop(&mut self) { + // `unblock` only ends `incoming_requests`; the port is released when the last `Arc` to the + // server is dropped, so the request thread has to be gone before the next test binds it + self.server.unblock(); + if let Some(requests) = self.requests.take() { + let _ = requests.join(); + } + } +} + +#[derive(Serialize)] +struct Config { + version: &'static str, + bundle: BundleConfig, + /// Merged into `plugins` of the app's `tauri.conf.json`. Only set by the signed version + /// tests, which need to flip `requireSignedVersion` and point at their own server. + #[serde(skip_serializing_if = "Option::is_none")] + plugins: Option, +} + +#[derive(Serialize)] +#[serde(rename_all = "camelCase")] +struct BundleConfig { + create_updater_artifacts: Updater, + /// Merged into `bundle.linux` of the app's `tauri.conf.json`. Only set by the RPM test, for + /// `rpm.provides` (see `rpm_self_provides`). + #[serde(skip_serializing_if = "Option::is_none")] + linux: Option, +} + +#[derive(Serialize)] +struct PlatformUpdate { + signature: String, + url: String, + with_elevated_task: bool, +} + +#[derive(Serialize)] +struct Update { + version: String, + date: String, + platforms: HashMap, +} + +fn build_app(cwd: &Path, config: &Config, target: Option) { + let mut command = Command::new("cargo"); + command + .args(["tauri", "build", "--verbose"]) + .arg("--config") + .arg(serde_json::to_string(config).unwrap()) + .env("TAURI_SIGNING_PRIVATE_KEY", UPDATER_PRIVATE_KEY) + .env("TAURI_SIGNING_PRIVATE_KEY_PASSWORD", "") + .current_dir(cwd); + + // linuxdeploy ships a `strip` that does not know the `.relr.dyn` sections recent distros emit, + // and it aborts the whole bundle when a strip call fails. Nothing here benefits from a smaller + // AppImage, so skip it. + #[cfg(target_os = "linux")] + command.env("NO_STRIP", "1"); + + if let Some(target) = target { + command.arg("--bundles").arg(target.name()); + } else { + command.arg("--no-bundle"); + } + + let status = command + .status() + .expect("failed to run Tauri CLI to bundle app"); + + if !status.success() { + panic!("failed to bundle app {:?}", status.code()); + } +} + +#[derive(Copy, Clone)] +enum BundleTarget { + AppImage, + Deb, + Rpm, + + App, + + Msi, + Nsis, +} + +impl BundleTarget { + fn name(self) -> &'static str { + match self { + Self::AppImage => "appimage", + Self::Deb => "deb", + Self::Rpm => "rpm", + Self::App => "app", + Self::Msi => "msi", + Self::Nsis => "nsis", + } + } +} + +#[cfg(target_os = "linux")] +impl BundleTarget { + /// Where `cargo tauri build --bundles ` leaves the bundle of the given app version. + fn bundle_path(self, root_dir: &Path, version: &str) -> PathBuf { + root_dir.join(match self { + Self::AppImage => { + format!("target/release/bundle/appimage/app-updater_{version}_amd64.AppImage") + } + Self::Deb => format!("target/release/bundle/deb/app-updater_{version}_amd64.deb"), + Self::Rpm => format!("target/release/bundle/rpm/app-updater-{version}-1.x86_64.rpm"), + _ => unreachable!("{} is not a Linux bundle", self.name()), + }) + } + + /// The package manager the updater hands this bundle to, for the Linux packages. + fn package_manager(self) -> Option<&'static str> { + match self { + Self::Deb => Some("dpkg"), + Self::Rpm => Some("rpm"), + _ => None, + } + } + + /// Whether this host can install the package without asking anything: the package manager must + /// be there with a database it can read, the runtime the app links against must be installed, + /// and `sudo` must not need a password. The update itself escalates through `pkexec`, then a + /// graphical password prompt, then `sudo`, and in a non-interactive run only the last one can + /// succeed. + fn installable(self) -> Result<(), String> { + let Some(manager) = self.package_manager() else { + return Ok(()); + }; + // listing the installed packages tells both that the manager is there and that its database + // works. `rpm` passes this on a Debian host too, against the empty database `apt install + // rpm` leaves behind; `rpm_self_provides` is what keeps the install from resolving the + // app's dependencies against it. + let list_packages: &[&str] = match self { + Self::Deb => &["-l"], + _ => &["-qa"], + }; + if !command_succeeds(manager, list_packages) { + return Err(format!( + "{manager} is not installed or its database is not readable" + )); + } + // so nothing checks webkit2gtk at install time, but the installed app still needs it to + // run. `ldconfig` lists it whichever manager put it there, and lives in an `sbin` that is + // not always on `PATH` + let webkit_installed = ["ldconfig", "/usr/sbin/ldconfig", "/sbin/ldconfig"] + .iter() + .any(|ldconfig| command_stdout_contains(ldconfig, &["-p"], "libwebkit2gtk-4.1.so.0")); + if !webkit_installed { + return Err("the webkit2gtk 4.1 runtime is not installed".into()); + } + if !command_succeeds("sudo", &["-n", "true"]) { + return Err("sudo needs a password".into()); + } + Ok(()) + } +} + +#[cfg(target_os = "linux")] +fn command_succeeds(program: &str, args: &[&str]) -> bool { + Command::new(program) + .args(args) + .stdout(Stdio::null()) + .stderr(Stdio::null()) + .status() + .map(|status| status.success()) + .unwrap_or(false) +} + +#[cfg(target_os = "linux")] +fn command_stdout_contains(program: &str, args: &[&str], needle: &str) -> bool { + Command::new(program) + .args(args) + .stderr(Stdio::null()) + .output() + .map(|output| String::from_utf8_lossy(&output.stdout).contains(needle)) + .unwrap_or(false) +} + +/// The libraries the Tauri CLI declares as RPM `Requires` of an app using wry, as +/// `Runtime::linux_dependencies` names them, with the suffix it appends on a 64 bit target. +#[cfg(target_os = "linux")] +const RPM_RUNTIME_LIBRARIES: &[&str] = + &["libwebkit2gtk-4.1.so.0()(64bit)", "libgtk-3.so.0()(64bit)"]; + +/// `bundle.linux` declaring the RPM `Requires` of `RPM_RUNTIME_LIBRARIES` the host's RPM database +/// cannot resolve as `Provides` of the package itself, or `None` when it resolves all of them (and +/// for a bundle that is not an RPM). +/// +/// `rpm` resolves `Requires` against its own database, and on a Debian host that database is the +/// empty one `apt install rpm` leaves behind: the libraries are there, installed by `apt` - which +/// is what `installable` checks with `ldconfig` - but nothing in the RPM database provides them, so +/// both the `rpm -i` that installs the app under test and the `rpm -U` the updater runs fail on +/// them. A package's own `Provides` satisfy its own `Requires`, so declaring them is what tells +/// `rpm` what the dynamic linker already knows. On an RPM distro the real packages provide them and +/// nothing is declared. +#[cfg(target_os = "linux")] +fn rpm_self_provides(bundle_target: BundleTarget) -> Option { + if !matches!(bundle_target, BundleTarget::Rpm) { + return None; + } + + let unresolved = RPM_RUNTIME_LIBRARIES + .iter() + .filter(|library| !command_succeeds("rpm", &["-q", "--whatprovides", library])) + .collect::>(); + if unresolved.is_empty() { + return None; + } + + Some(serde_json::json!({ "rpm": { "provides": unresolved } })) +} + +/// Installs the package system wide, which is where the updater will later install the update over +/// it, and returns the path of the installed executable. +#[cfg(target_os = "linux")] +fn install_package(bundle_target: BundleTarget, package: &Path) -> PathBuf { + let manager = bundle_target + .package_manager() + .expect("not a Linux package"); + // whatever an earlier run left behind + remove_package(bundle_target); + let status = Command::new("sudo") + .args(["-n", manager, "-i"]) + .arg(package) + .status() + .expect("failed to run sudo"); + assert!(status.success(), "failed to install {}", package.display()); + PathBuf::from("/usr/bin/app-updater") +} + +#[cfg(target_os = "linux")] +fn remove_package(bundle_target: BundleTarget) { + let (manager, remove) = match bundle_target { + BundleTarget::Deb => ("dpkg", "-r"), + BundleTarget::Rpm => ("rpm", "-e"), + _ => return, + }; + let _ = Command::new("sudo") + .args(["-n", manager, remove, "app-updater"]) + .stdout(Stdio::null()) + .stderr(Stdio::null()) + .status(); +} + +impl Default for BundleTarget { + fn default() -> Self { + #[cfg(any(target_os = "macos", target_os = "ios"))] + return Self::App; + #[cfg(target_os = "linux")] + return Self::AppImage; + #[cfg(windows)] + return Self::Nsis; + } +} + +/// Copies a bundle out of the bundler output directory, returning the path to run. +/// +/// Every bundler wipes its output before writing — the AppImage one removes `bundle/appimage` +/// entirely, the macOS one the `.app` — so the app built for the initial version only survives +/// until the next `build_app` call. On Linux and macOS the update is also installed over the very +/// bundle being run, which would otherwise leave a 1.0.0 app behind in the bundler output. +/// +/// Windows runs a copy of the executable `cargo build` leaves in `target/release`, patched with the +/// bundle type (see `patch_bundle_type`). The NSIS update installs into the directory of the +/// running executable, so the copy also keeps the install out of `target/release`. +/// +/// A Linux package is not copied but installed: the updater hands the downloaded package to the +/// package manager, which installs it over the one the app runs from. +fn stage_app_under_test(root_dir: &Path, target: &str, bundle_target: BundleTarget) -> PathBuf { + #[cfg(windows)] + { + let _ = target; + let staging_dir = root_dir.join("target/release/app-under-test"); + let _ = std::fs::remove_dir_all(&staging_dir); + std::fs::create_dir_all(&staging_dir).expect("failed to create the staging directory"); + + let staged = staging_dir.join("app-updater.exe"); + patch_bundle_type(&app_binary(root_dir), &staged, bundle_target); + return staged; + } + + #[cfg(target_os = "linux")] + { + if bundle_target.package_manager().is_some() { + return install_package(bundle_target, &bundle_target.bundle_path(root_dir, "0.1.0")); + } + } + + #[cfg(not(windows))] + { + let _ = bundle_target; + let bundle_path = test_cases(root_dir, "0.1.0", target.to_string()) + .first() + .unwrap() + .1 + .clone(); + + let staging_dir = root_dir.join("target/release/app-under-test"); + let _ = std::fs::remove_dir_all(&staging_dir); + std::fs::create_dir_all(&staging_dir).expect("failed to create the staging directory"); + + let staged = staging_dir.join(bundle_path.file_name().unwrap()); + copy_recursively(&bundle_path, &staged).unwrap_or_else(|e| { + panic!( + "failed to copy {} to {}: {e}", + bundle_path.display(), + staged.display() + ) + }); + + return staged; + } +} + +/// Copies `from` to `to` with the bundle type the updater reads through `bundle_type()` set to +/// `bundle_target`, the way the Tauri CLI does it: by overwriting the first +/// `__TAURI_BUNDLE_TYPE_VAR_UNK` in the executable. +/// +/// The executable in `target/release` never carries it. The CLI patches it only for the time it +/// takes to put it in the installer, then restores the unpatched one, so without this the app +/// cannot resolve the `{target}-{bundle_type}` platform key and only finds `{target}`. +#[cfg(windows)] +fn patch_bundle_type(from: &Path, to: &Path, bundle_target: BundleTarget) { + const PLACEHOLDER: &[u8] = b"__TAURI_BUNDLE_TYPE_VAR_UNK"; + let bundle_type: &[u8] = match bundle_target { + BundleTarget::Nsis => b"__TAURI_BUNDLE_TYPE_VAR_NSS", + BundleTarget::Msi => b"__TAURI_BUNDLE_TYPE_VAR_MSI", + _ => unreachable!("{} is not a Windows bundle", bundle_target.name()), + }; + + let mut binary = + std::fs::read(from).unwrap_or_else(|e| panic!("failed to read {}: {e}", from.display())); + let index = binary + .windows(PLACEHOLDER.len()) + .position(|window| window == PLACEHOLDER) + .unwrap_or_else(|| panic!("no bundle type placeholder in {}", from.display())); + binary[index..index + PLACEHOLDER.len()].copy_from_slice(bundle_type); + std::fs::write(to, binary).unwrap_or_else(|e| panic!("failed to write {}: {e}", to.display())); +} + +/// Copies a file, or a directory such as a macOS `.app`, preserving permissions. +fn copy_recursively(from: &Path, to: &Path) -> std::io::Result<()> { + if from.is_dir() { + std::fs::create_dir_all(to)?; + for entry in std::fs::read_dir(from)? { + let entry = entry?; + copy_recursively(&entry.path(), &to.join(entry.file_name()))?; + } + Ok(()) + } else { + std::fs::copy(from, to).map(|_| ()) + } +} + +fn target_to_platforms( + update_platform: Option, + signature: String, +) -> HashMap { + let mut platforms = HashMap::new(); + if let Some(platform) = update_platform { + platforms.insert( + platform, + PlatformUpdate { + signature, + url: format!("http://localhost:{UPDATE_PORT}/download"), + with_elevated_task: false, + }, + ); + } + + platforms +} + +/// A bundle to build, the update platform key the server announces it under (none for a manifest +/// without an update) and the exit codes expected from running the app once per entry. +type TestCase = (BundleTarget, PathBuf, Option, Vec); + +#[cfg(target_os = "linux")] +fn test_cases(root_dir: &Path, version: &str, target: String) -> Vec { + let appimage = BundleTarget::AppImage.bundle_path(root_dir, version); + vec![ + // update using fallback + ( + BundleTarget::AppImage, + appimage.clone(), + Some(target.clone()), + vec![UPDATED_EXIT_CODE, UP_TO_DATE_EXIT_CODE], + ), + // update using full name + ( + BundleTarget::AppImage, + appimage.clone(), + Some(format!("{target}-{}", BundleTarget::AppImage.name())), + vec![UPDATED_EXIT_CODE, UP_TO_DATE_EXIT_CODE], + ), + // no update + ( + BundleTarget::AppImage, + appimage, + None, + vec![ERROR_EXIT_CODE], + ), + ] +} + +/// The cases of a Linux package: an update announced under the generic target and one under the +/// package specific target. There is no "no update" case, the AppImage one covers that path. +#[cfg(target_os = "linux")] +fn package_test_cases( + root_dir: &Path, + version: &str, + target: String, + bundle_target: BundleTarget, +) -> Vec { + let package = bundle_target.bundle_path(root_dir, version); + vec![ + // update using fallback + ( + bundle_target, + package.clone(), + Some(target.clone()), + vec![UPDATED_EXIT_CODE, UP_TO_DATE_EXIT_CODE], + ), + // update using full name + ( + bundle_target, + package, + Some(format!("{target}-{}", bundle_target.name())), + vec![UPDATED_EXIT_CODE, UP_TO_DATE_EXIT_CODE], + ), + ] +} + +#[cfg(target_os = "macos")] +fn test_cases(root_dir: &Path, _version: &str, target: String) -> Vec { + vec![ + ( + BundleTarget::App, + root_dir.join("target/release/bundle/macos/app-updater.app"), + Some(target.clone()), + vec![UPDATED_EXIT_CODE, UP_TO_DATE_EXIT_CODE], + ), + // update with installer + ( + BundleTarget::App, + root_dir.join("target/release/bundle/macos/app-updater.app"), + Some(format!("{target}-{}", BundleTarget::App.name())), + vec![UPDATED_EXIT_CODE, UP_TO_DATE_EXIT_CODE], + ), + // no update + ( + BundleTarget::App, + root_dir.join("target/release/bundle/macos/app-updater.app"), + None, + vec![ERROR_EXIT_CODE], + ), + ] +} + +#[cfg(target_os = "ios")] +fn bundle_paths( + root_dir: &Path, + _version: &str, + v1compatible: bool, +) -> Vec<(BundleTarget, PathBuf)> { + vec![( + BundleTarget::App, + root_dir.join("target/release/bundle/ios/app-updater.ipa"), + )] +} + +#[cfg(target_os = "android")] +fn bundle_path(root_dir: &Path, _version: &str, v1compatible: bool) -> PathBuf { + root_dir.join("target/release/bundle/android/app-updater.apk") +} + +#[cfg(windows)] +fn test_cases(root_dir: &Path, version: &str, target: String) -> Vec { + vec![ + ( + BundleTarget::Nsis, + root_dir.join(format!( + "target/release/bundle/nsis/app-updater_{version}_x64-setup.exe" + )), + Some(target.clone()), + vec![UPDATED_EXIT_CODE], + ), + ( + BundleTarget::Nsis, + root_dir.join(format!( + "target/release/bundle/nsis/app-updater_{version}_x64-setup.exe" + )), + Some(format!("{target}-{}", BundleTarget::Nsis.name())), + vec![UPDATED_EXIT_CODE], + ), + ( + BundleTarget::Nsis, + root_dir.join(format!( + "target/release/bundle/nsis/app-updater_{version}_x64-setup.exe" + )), + None, + vec![ERROR_EXIT_CODE], + ), + ( + BundleTarget::Msi, + root_dir.join(format!( + "target/release/bundle/msi/app-updater_{version}_x64_en-US.msi" + )), + Some(target.clone()), + vec![UPDATED_EXIT_CODE], + ), + ( + BundleTarget::Msi, + root_dir.join(format!( + "target/release/bundle/msi/app-updater_{version}_x64_en-US.msi" + )), + Some(format!("{target}-{}", BundleTarget::Msi.name())), + vec![UPDATED_EXIT_CODE], + ), + ( + BundleTarget::Msi, + root_dir.join(format!( + "target/release/bundle/msi/app-updater_{version}_x64_en-US.msi" + )), + None, + vec![ERROR_EXIT_CODE], + ), + ] +} + +#[test] +#[ignore = "needs the tauri CLI, a display and minutes of build time; the integration tests workflow runs it with --ignored"] +fn update_app() { + let _lock = BUILD_LOCK.lock().unwrap_or_else(|e| e.into_inner()); + + let target = + tauri_plugin_updater::target().expect("running updater test in an unsupported platform"); + let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")); + let root_dir = manifest_dir.join("../../../.."); + + for config in [ + Config { + version: "1.0.0", + bundle: BundleConfig { + create_updater_artifacts: Updater::Bool(true), + linux: None, + }, + plugins: None, + }, + Config { + version: "1.0.0", + bundle: BundleConfig { + create_updater_artifacts: Updater::String(V1Compatible::V1Compatible), + linux: None, + }, + plugins: None, + }, + ] { + let cases = test_cases(&root_dir, "1.0.0", target.clone()); + run_update_cases(&manifest_dir, &root_dir, &target, config, cases); + } +} + +/// The updater installs a deb through `dpkg` and an rpm through `rpm`, so this runs the installed +/// app and lets the update replace it system wide. It only runs where that can happen without any +/// prompt (see `BundleTarget::installable`) and removes the package when done. +#[cfg(target_os = "linux")] +#[test] +#[ignore = "needs the tauri CLI, a display and minutes of build time; the integration tests workflow runs it with --ignored"] +fn update_app_deb() { + update_linux_package(BundleTarget::Deb); +} + +/// See `update_app_deb`. +#[cfg(target_os = "linux")] +#[test] +#[ignore = "needs the tauri CLI, a display and minutes of build time; the integration tests workflow runs it with --ignored"] +fn update_app_rpm() { + update_linux_package(BundleTarget::Rpm); +} + +#[cfg(target_os = "linux")] +fn update_linux_package(bundle_target: BundleTarget) { + let _lock = BUILD_LOCK.lock().unwrap_or_else(|e| e.into_inner()); + + if let Err(reason) = bundle_target.installable() { + // the integration tests workflow installs everything this needs, so a skip there means the + // test quietly stopped covering the package - libtest swallows the message on a pass + assert!( + std::env::var_os("CI").is_none(), + "cannot run the {} update test: {reason}", + bundle_target.name() + ); + eprintln!( + "skipping the {} update test: {reason}", + bundle_target.name() + ); + return; + } + + let target = + tauri_plugin_updater::target().expect("running updater test in an unsupported platform"); + let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")); + let root_dir = manifest_dir.join("../../../.."); + + // the legacy v1 updater never supported these packages, so only the v2 artifacts are tested + let config = Config { + version: "1.0.0", + bundle: BundleConfig { + create_updater_artifacts: Updater::Bool(true), + linux: rpm_self_provides(bundle_target), + }, + plugins: None, + }; + let cases = package_test_cases(&root_dir, "1.0.0", target.clone(), bundle_target); + run_update_cases(&manifest_dir, &root_dir, &target, config, cases); +} + +/// Bundles 1.0.0 as each case's target and serves it, then bundles 0.1.0, runs it once per expected +/// exit code and checks what it reports. +fn run_update_cases( + manifest_dir: &Path, + root_dir: &Path, + target: &str, + mut config: Config, + cases: Vec, +) { + let v1_compatible = matches!( + config.bundle.create_updater_artifacts, + Updater::String(V1Compatible::V1Compatible) + ); + + let updater_zip_ext = if v1_compatible { + if cfg!(windows) { + Some("zip") + } else { + Some("tar.gz") + } + } else if cfg!(target_os = "macos") { + Some("tar.gz") + } else { + None + }; + + for (bundle_target, out_bundle_path, update_platform, status_checks) in cases { + // bundle app update + config.version = "1.0.0"; + build_app(manifest_dir, &config, Some(bundle_target)); + + let bundle_updater_ext = if v1_compatible { + out_bundle_path + .extension() + .unwrap() + .to_str() + .unwrap() + .replace("exe", "nsis") + } else { + out_bundle_path + .extension() + .unwrap() + .to_str() + .unwrap() + .to_string() + }; + let updater_extension = if let Some(updater_zip_ext) = updater_zip_ext { + format!("{bundle_updater_ext}.{updater_zip_ext}") + } else { + bundle_updater_ext + }; + let signature_extension = format!("{updater_extension}.sig"); + let signature_path = out_bundle_path.with_extension(signature_extension); + let signature = std::fs::read_to_string(&signature_path).unwrap_or_else(|_| { + panic!("failed to read signature file {}", signature_path.display()) + }); + let out_updater_path = out_bundle_path.with_extension(updater_extension); + let updater_path = root_dir.join(format!( + "target/release/{}", + out_updater_path.file_name().unwrap().to_str().unwrap() + )); + std::fs::rename(&out_updater_path, &updater_path).expect("failed to rename bundle"); + + // start the updater server, shut down at the end of the iteration even if a case panics + let _server = UpdaterServer::spawn(UPDATE_PORT, move |request| match request.url() { + "/" => { + let platforms = target_to_platforms(update_platform.clone(), signature.clone()); + + let body = serde_json::to_vec(&Update { + version: "1.0.0".into(), + date: time::OffsetDateTime::now_utc() + .format(&time::format_description::well_known::Rfc3339) + .unwrap(), + platforms, + }) + .unwrap(); + let len = body.len(); + let response = tiny_http::Response::new( + tiny_http::StatusCode(200), + Vec::new(), + std::io::Cursor::new(body), + Some(len), + None, + ); + let _ = request.respond(response); + } + "/download" => { + let _ = request.respond(tiny_http::Response::from_file( + File::open(&updater_path).unwrap_or_else(|_| { + panic!("failed to open updater bundle {}", updater_path.display()) + }), + )); + } + _ => (), + }); + + config.version = "0.1.0"; + + // bundle initial app version; Linux and macOS run the bundle itself, so it cannot be + // skipped there + build_app( + manifest_dir, + &config, + if cfg!(windows) { + None + } else { + Some(bundle_target) + }, + ); + + let app_path = stage_app_under_test(root_dir, target, bundle_target); + + for expected_exit_code in status_checks { + let mut binary_cmd = if cfg!(target_os = "macos") { + Command::new(app_path.join("Contents/MacOS/app-updater")) + } else if cfg!(target_os = "linux") + && std::env::var("CI").map(|v| v == "true").unwrap_or_default() + { + let mut c = Command::new("xvfb-run"); + c.arg("--auto-servernum").arg(&app_path); + c + } else { + Command::new(&app_path) + }; + + binary_cmd.env("TARGET", bundle_target.name()); + + let status = binary_cmd + .status() + .unwrap_or_else(|e| panic!("failed to run {}: {e}", app_path.display())); + let code = status.code().unwrap_or(-1); + + if code != expected_exit_code { + panic!( + "failed to run app bundled as {}, expected exit code {expected_exit_code}, got {code}", bundle_target.name() + ); + } + #[cfg(windows)] + if code == UPDATED_EXIT_CODE { + // wait for the update to finish + std::thread::sleep(std::time::Duration::from_secs(5)); + } + } + + #[cfg(target_os = "linux")] + { + remove_package(bundle_target); + } + } +} + +const SIGNED_VERSION_PORT: u16 = 3008; +/// Fragment of `Error::SignedVersionMismatch`. The app prints the updater error before exiting, +/// and a rejected update exits with the same code as a failed install, so the message is what +/// tells them apart. +const MISMATCH_ERROR: &str = "was signed for version"; + +struct ServedUpdate { + version: String, +} + +fn app_binary(root_dir: &Path) -> PathBuf { + root_dir.join(if cfg!(windows) { + "target/release/app-updater.exe" + } else { + "target/release/app-updater" + }) +} + +/// How the app under test has to be built for an install to stay contained. +/// +/// Windows and Linux install over the executable itself, so a bare `cargo build` is enough there. +/// macOS replaces the whole `.app` the running executable sits in, and for an executable that is +/// not in one the updater falls back to the directory merely holding it — `target/release`, which +/// is also where the bundle being served and the build cache live. So macOS bundles the app and +/// runs it out of a staged copy of that `.app`, which the install is welcome to replace. +#[cfg(target_os = "macos")] +const APP_UNDER_TEST_BUNDLE: Option = Some(BundleTarget::App); +#[cfg(not(target_os = "macos"))] +const APP_UNDER_TEST_BUNDLE: Option = None; + +/// Where `build_app` leaves the app under test. +fn built_app(root_dir: &Path) -> PathBuf { + #[cfg(target_os = "macos")] + return root_dir.join("target/release/bundle/macos/app-updater.app"); + #[cfg(not(target_os = "macos"))] + return app_binary(root_dir); +} + +/// The copy the test runs, kept out of the way of both the bundler and the build cache. +fn staged_app(root_dir: &Path) -> PathBuf { + #[cfg(target_os = "macos")] + return root_dir.join("target/release/app-under-test-signed/app-updater.app"); + #[cfg(not(target_os = "macos"))] + return app_binary(root_dir); +} + +/// The executable to launch inside the staged app. +fn staged_binary(root_dir: &Path) -> PathBuf { + #[cfg(target_os = "macos")] + return staged_app(root_dir).join("Contents/MacOS/app-updater"); + #[cfg(not(target_os = "macos"))] + return staged_app(root_dir); +} + +/// Puts the pristine 0.1.0 app back where the test runs it, replacing whatever an install left. +fn restore_app(root_dir: &Path, pristine: &Path) { + let app = staged_app(root_dir); + let _ = std::fs::remove_dir_all(&app); + let _ = std::fs::remove_file(&app); + std::fs::create_dir_all(app.parent().unwrap()).expect("failed to create the staging directory"); + copy_recursively(pristine, &app).unwrap_or_else(|e| { + panic!( + "failed to restore {} from {}: {e}", + app.display(), + pristine.display() + ) + }); +} + +/// Runs the app against the update server, restoring it from `pristine` first. +/// +/// A case that clears the version check goes on to actually install, and installing means writing +/// over what the app runs from. Every case therefore has to start from an untouched copy, or the +/// first one that passes leaves the update behind as the app. +fn run_app(root_dir: &Path, pristine: &Path) -> String { + restore_app(root_dir, pristine); + let binary = staged_binary(root_dir); + + let mut command = if cfg!(target_os = "linux") + && std::env::var("CI").map(|v| v == "true").unwrap_or_default() + { + let mut c = Command::new("xvfb-run"); + c.arg("--auto-servernum").arg(&binary); + c + } else { + Command::new(&binary) + }; + + let output = command.output().expect("failed to run app"); + String::from_utf8_lossy(&output.stdout).into_owned() +} + +/// The update endpoint response is not signed, so its `version` field alone does not prove which +/// release the `url` and `signature` point at. This bundles a genuine 1.0.0 release and then varies +/// only the version the endpoint announces for it, which is the shape of a forced downgrade: a +/// tampered response pairing a new version number with an older release's signature. +/// +/// The check runs in `Update::download`, before anything is installed, so a rejected case never +/// reaches the installer while an accepted one does. Both exit non-zero, which is why these assert +/// on the error message rather than the exit code. +#[test] +#[ignore = "needs the tauri CLI, a display and minutes of build time; the integration tests workflow runs it with --ignored"] +fn update_validates_signed_version() { + let _lock = BUILD_LOCK.lock().unwrap_or_else(|e| e.into_inner()); + + let target = + tauri_plugin_updater::target().expect("running updater test in an unsupported platform"); + let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")); + let root_dir = manifest_dir.join("../../../.."); + + // bundle the release the endpoint will point at; the CLI records 1.0.0 in its signature + let bundle_target = BundleTarget::default(); + build_app( + &manifest_dir, + &Config { + version: "1.0.0", + bundle: BundleConfig { + create_updater_artifacts: Updater::Bool(true), + linux: None, + }, + plugins: None, + }, + Some(bundle_target), + ); + + let out_bundle_path = test_cases(&root_dir, "1.0.0", target.clone()) + .first() + .unwrap() + .1 + .clone(); + let updater_extension = { + let bundle_ext = out_bundle_path + .extension() + .unwrap() + .to_str() + .unwrap() + .to_string(); + if cfg!(target_os = "macos") { + format!("{bundle_ext}.tar.gz") + } else { + bundle_ext + } + }; + let signature = + std::fs::read_to_string(out_bundle_path.with_extension(format!("{updater_extension}.sig"))) + .expect("failed to read signature file"); + + // move it aside so building the running app cannot clobber it + let out_updater_path = out_bundle_path.with_extension(&updater_extension); + let updater_path = root_dir.join(format!( + "target/release/{}", + out_updater_path.file_name().unwrap().to_str().unwrap() + )); + std::fs::rename(&out_updater_path, &updater_path).expect("failed to rename bundle"); + + let served = Arc::new(Mutex::new(ServedUpdate { + version: "1.0.0".into(), + })); + + let served_ = served.clone(); + let target_ = target.clone(); + let updater_path_ = updater_path.clone(); + // shut down at the end of the test even if a check below panics + let _server = UpdaterServer::spawn(SIGNED_VERSION_PORT, move |request| match request.url() { + "/" => { + let mut platforms = HashMap::new(); + platforms.insert( + target_.clone(), + PlatformUpdate { + // always the genuine 1.0.0 signature; only the version above it moves + signature: signature.clone(), + url: format!("http://localhost:{SIGNED_VERSION_PORT}/download"), + with_elevated_task: false, + }, + ); + + let body = serde_json::to_vec(&Update { + version: served_.lock().unwrap().version.clone(), + date: time::OffsetDateTime::now_utc() + .format(&time::format_description::well_known::Rfc3339) + .unwrap(), + platforms, + }) + .unwrap(); + let len = body.len(); + let _ = request.respond(tiny_http::Response::new( + tiny_http::StatusCode(200), + Vec::new(), + std::io::Cursor::new(body), + Some(len), + None, + )); + } + "/download" => { + let _ = request.respond(tiny_http::Response::from_file( + File::open(&updater_path_).unwrap_or_else(|_| { + panic!("failed to open updater bundle {}", updater_path_.display()) + }), + )); + } + _ => (), + }); + + // `requireSignedVersion` is baked in by `generate_context!`, so each value needs its own build + let pristine = root_dir.join("target/release/app-updater.pristine"); + let build = |require_signed_version: bool| { + build_app( + &manifest_dir, + &Config { + version: "0.1.0", + bundle: BundleConfig { + create_updater_artifacts: Updater::Bool(true), + linux: None, + }, + plugins: Some(serde_json::json!({ + "updater": { + "endpoints": [format!("http://localhost:{SIGNED_VERSION_PORT}")], + "requireSignedVersion": require_signed_version, + } + })), + }, + APP_UNDER_TEST_BUNDLE, + ); + let _ = std::fs::remove_dir_all(&pristine); + let _ = std::fs::remove_file(&pristine); + copy_recursively(&built_app(&root_dir), &pristine) + .expect("failed to keep a pristine copy of the app"); + }; + + let check = |announced: &str, expected: Option<&str>, unexpected: Option<&str>| { + served.lock().unwrap().version = announced.to_string(); + let output = run_app(&root_dir, &pristine); + + if let Some(expected) = expected { + assert!( + output.contains(expected), + "expected {expected:?} when announcing {announced} for a 1.0.0 signature, got: {output}" + ); + } + if let Some(unexpected) = unexpected { + assert!( + !output.contains(unexpected), + "unexpected {unexpected:?} when announcing {announced} for a 1.0.0 signature, got: {output}" + ); + } + }; + + build(true); + + // the rollback this exists to stop: a 1.0.0 release dressed up as a newer one + check("1.5.0", Some(MISMATCH_ERROR), None); + // the announced version is what the artifact was signed for, so it must go through + check("1.0.0", None, Some(MISMATCH_ERROR)); + + build(false); + + // a signature that names a version is held to it whether or not the option is on + check("1.5.0", Some(MISMATCH_ERROR), None); + + // leave the 0.1.0 app behind rather than whichever update was installed last + restore_app(&root_dir, &pristine); + let _ = std::fs::remove_dir_all(&pristine); + let _ = std::fs::remove_file(&pristine); +} diff --git a/plugins/updater/tests/updater-migration/Cargo.toml b/plugins/updater/tests/updater-migration/Cargo.toml new file mode 100644 index 0000000..680b4e9 --- /dev/null +++ b/plugins/updater/tests/updater-migration/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "updater-migration-test" +version = "0.1.0" +edition = { workspace = true } + +[build-dependencies] +tauri-build = { workspace = true } + +[dependencies] +tauri = { workspace = true, features = ["wry", "common-controls-v6", "x11"] } +serde = { workspace = true } +serde_json = { workspace = true } +tauri-plugin-updater = { path = "../.." } +tiny_http = "0.12" +time = { version = "0.3", features = ["formatting"] } diff --git a/plugins/updater/tests/updater-migration/icons/128x128.png b/plugins/updater/tests/updater-migration/icons/128x128.png new file mode 100644 index 0000000000000000000000000000000000000000..77e7d2338e9d8ccffc731198dc584906627c903f GIT binary patch literal 11059 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

+

Welcome to Tauri!

+ +
+ +

Click on the Tauri logo to learn more about the framework

+ + + +

+

5l#v3A#xI3*z~1~fvo9Q(N^(==!|_FZ z*duZ=+M1~)8E|otX8KNZlr?qels#x_1Xq@9IIw~@9uAREJVH)Xw^}UclF6327}E42 zT)E&?U%TK?(+K7%R!`H5oX0i)4Qn5??Iw3p5J~6_u+aWehY{DSn}3V2p$bgjnAu?o)v@iC254fXeMv50$9YrpU`N?u@QIWs)T?SP|fa}(|9 zqAX+!7`cx=4)cCBg5h~pu(?@9`)aCr#oyz$ld=#RFxYCNZCZls@4v2~*e-t6PEVvV z&bbK3b3wt(Coc!ufAbXXC<**#HQ%J9k`New6iG<5RjtO4XVO?dCvwxD{kJ#tfQr(X zg^NTwF-FwAeS_{V4bfel8l`~NbfrTR2s!G>WduFWxH(t~aK4q=6rEE^$+Uox>gJO2 z{L<;6Q6nHa5#ZEM>H58not!)z(6*_=^~8}jWf*IG$AUKVWOZ4?)GfF z+BM#*wKKmLFD7E~W3U!$IVm$k_k1f&Kz6WV8@55P?r~bcg-Za-!rvW?ns&)KOGT2~ zlkAyqhQj=P$Eg3w#K~}zH@J5bo-BfHjInKSz$@?+Z)NPD4pHj^_Qxmi`UqoTy=`sV zLVxrXGuBr=QRm|}wg75yetQQK4fY3#P_~J}zEfPnb2C4Wo!E(d*(cA;b?7$g2in<( zPn)ghX}nzJPmb6(3Dpeg_GW~Hc}Lt=lgsSZz z!5QXyz7KaR;D`3Ee}d`af{H>WWZ|Io1QI3~4Ll_`g1(cRnhLK73Ro)7zPCd={1W2x zRp%Xlvv4>!<2@}$hz|!V{T}_eHx2xkLl^hQoZTCnsjCl|W_@5Fx2(+j0ogy&Y+;L- z<)G$*CiN7hOm^s!{U>1F7U=iNk{+u~dAC!eDz%=|glFW0jEZU1&o(G_c#wTxUjnG} z#cg3>jEpUi#Mlq@t?Msg_#geK^Lx@DyHWf7=AS5vVyM7YOjvUVCfcpVR<(+5!H?9- zySI6s>o3m&*zr||=wcPGyBkQV`EWJl@bH8qobjOp+sXL*)=&yX)8aAbf~tGv?a2SN zu^Ddo-z?DWk9h9Yz#5p^NU#x~wYSd?H@w@!2Gb4G)6-utEMV~~M85Br5ff(v5O1|T z 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 = + dyn FnMut(&AppHandle, Vec, String) + Send + Sync + 'static; + +/// Initializes the plugin, calling `f` whenever a second instance of the app is started. +/// +/// This is a shortcut for [`Builder::new`] with [`Builder::callback`] set to `f`, then +/// [`Builder::build`]. Use [`Builder`] directly if you need to set a custom [`Builder::dbus_id`]. +/// +/// `f` is called with the app handle, the second instance's command line arguments +/// (as collected by [`std::env::args`], so the first element is the executable path) and its +/// current working directory. If the `deep-link` feature is enabled, the arguments are first +/// forwarded to [`tauri-plugin-deep-link`](https://crates.io/crates/tauri-plugin-deep-link) +/// before `f` runs. +/// +/// The second instance never reaches [`tauri::Builder::run`]: it hands its arguments and working +/// directory off to the first instance and exits immediately. +pub fn init, Vec, String) + Send + Sync + 'static>( + f: F, +) -> TauriPlugin { + Builder::new().callback(f).build() +} + +/// Releases the resources this plugin uses to detect other instances (the named mutex on +/// Windows, the D-Bus name on Linux or the Unix socket on macOS). +/// +/// The plugin calls this automatically on [`tauri::RunEvent::Exit`], so you normally don't need +/// to call it yourself. Call it manually before terminating the process through means that skip +/// that event, such as [`std::process::exit`], so a future instance of the app isn't mistaken +/// for a still-running one. +pub fn destroy>(manager: &M) { + platform_impl::destroy(manager) +} + +/// Builds the single-instance plugin. +/// +/// Created with [`Builder::new`] and consumed by [`Builder::build`]. +pub struct Builder { + callback: Box>, + dbus_id: Option, +} + +impl Default for Builder { + fn default() -> Self { + Self { + callback: Box::new(move |_app, _args, _| { + #[cfg(feature = "deep-link")] + if let Some(deep_link) = _app.try_state::>() { + deep_link.handle_cli_arguments(_args.iter()); + } + }), + dbus_id: None, + } + } +} + +impl Builder { + /// Creates a new builder with a no-op callback (or, when the `deep-link` feature is enabled, + /// a callback that only forwards the arguments to the deep-link plugin) and no custom D-Bus + /// ID. Use [`Builder::callback`] and [`Builder::dbus_id`] to configure it, then + /// [`Builder::build`] to create the plugin. + pub fn new() -> Self { + Default::default() + } + + /// Function to call when a secondary instance was opened by the user and killed by the plugin. + /// If the `deep-link` feature is enabled, the plugin triggers the deep-link plugin before executing the callback. + pub fn callback, Vec, String) + Send + Sync + 'static>( + mut self, + mut f: F, + ) -> Self { + self.callback = Box::new(move |app, args, cwd| { + #[cfg(feature = "deep-link")] + if let Some(deep_link) = app.try_state::>() { + deep_link.handle_cli_arguments(args.iter()); + } + f(app, args, cwd) + }); + self + } + + /// Set a custom D-Bus ID, used on Linux. The plugin will append a `.SingleInstance` subname. + /// For example `com.mycompany.myapp` will result in the plugin registering its D-Bus service on `com.mycompany.myapp.SingleInstance`. + /// Usually you want the same base ID across all components in your app. + /// + /// Defaults to the app's bundle identifier set in tauri.conf.json. + pub fn dbus_id(mut self, dbus_id: impl Into) -> Self { + self.dbus_id = Some(dbus_id.into()); + self + } + + /// Builds the single-instance [`TauriPlugin`]. + /// + /// Register it first among your app's plugins, since the plugins run in the order they were + /// added and a second instance must be detected before the rest of your setup runs. + pub fn build(self) -> TauriPlugin { + platform_impl::init( + self.callback, + #[cfg(target_os = "linux")] + self.dbus_id, + ) + } +} diff --git a/plugins/single-instance/src/platform_impl/linux.rs b/plugins/single-instance/src/platform_impl/linux.rs new file mode 100644 index 0000000..892baee --- /dev/null +++ b/plugins/single-instance/src/platform_impl/linux.rs @@ -0,0 +1,112 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +#[cfg(feature = "semver")] +use crate::semver_compat::semver_compat_string; + +use crate::SingleInstanceCallback; +use tauri::{ + plugin::{self, TauriPlugin}, + AppHandle, Manager, RunEvent, Runtime, +}; +use zbus::{blocking::Connection, interface, names::WellKnownName}; + +struct ConnectionHandle(Connection); + +struct SingleInstanceDBus { + callback: Box>, + app_handle: AppHandle, +} + +#[interface(name = "org.SingleInstance.DBus")] +impl SingleInstanceDBus { + fn execute_callback(&mut self, argv: Vec, cwd: String) { + (self.callback)(&self.app_handle, argv, cwd); + } +} + +struct DBusName(String); + +pub fn init( + callback: Box>, + dbus_id: Option, +) -> TauriPlugin { + plugin::Builder::new("single-instance") + .setup(move |app, _api| { + let mut dbus_name = dbus_id.unwrap_or_else(|| app.config().identifier.clone()); + dbus_name.push_str(".SingleInstance"); + + #[cfg(feature = "semver")] + { + dbus_name.push('_'); + dbus_name.push_str(semver_compat_string(&app.package_info().version).as_str()); + } + + let mut dbus_path = dbus_name.replace('.', "/").replace('-', "_"); + if !dbus_path.starts_with('/') { + dbus_path = format!("/{dbus_path}"); + } + + let single_instance_dbus = SingleInstanceDBus { + callback, + app_handle: app.clone(), + }; + + match zbus::blocking::connection::Builder::session() + .unwrap() + .name(dbus_name.as_str()) + .unwrap() + .replace_existing_names(false) + .allow_name_replacements(false) + .serve_at(dbus_path.as_str(), single_instance_dbus) + .unwrap() + .build() + { + Ok(connection) => { + app.manage(ConnectionHandle(connection)); + } + Err(zbus::Error::NameTaken) => { + if let Ok(connection) = Connection::session() { + let _ = connection.call_method( + Some(dbus_name.as_str()), + dbus_path.as_str(), + Some("org.SingleInstance.DBus"), + "ExecuteCallback", + &( + std::env::args().collect::>(), + std::env::current_dir() + .unwrap_or_default() + .to_str() + .unwrap_or_default(), + ), + ); + } + app.cleanup_before_exit(); + std::process::exit(0); + } + _ => {} + } + + app.manage(DBusName(dbus_name)); + + Ok(()) + }) + .on_event(move |app, event| { + if let RunEvent::Exit = event { + destroy(app); + } + }) + .build() +} + +pub fn destroy>(manager: &M) { + if let Some(connection) = manager.try_state::() { + if let Some(dbus_name) = manager + .try_state::() + .and_then(|name| WellKnownName::try_from(name.0.clone()).ok()) + { + let _ = connection.0.release_name(dbus_name); + } + } +} diff --git a/plugins/single-instance/src/platform_impl/macos.rs b/plugins/single-instance/src/platform_impl/macos.rs new file mode 100644 index 0000000..bd2c74a --- /dev/null +++ b/plugins/single-instance/src/platform_impl/macos.rs @@ -0,0 +1,132 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use std::{ + io::{BufWriter, Error, ErrorKind, Write}, + os::unix::net::UnixStream, + path::PathBuf, +}; + +#[cfg(feature = "semver")] +use crate::semver_compat::semver_compat_string; +use crate::SingleInstanceCallback; +use tauri::{ + plugin::{self, TauriPlugin}, + AppHandle, Config, Manager, RunEvent, Runtime, +}; +use tokio::io::AsyncReadExt; + +pub fn init(cb: Box>) -> TauriPlugin { + plugin::Builder::new("single-instance") + .setup(|app, _api| { + let socket = socket_path(app.config(), app.package_info()); + + // Notify the singleton which may or may not exist. + match notify_singleton(&socket) { + Ok(_) => { + std::process::exit(0); + } + Err(e) => { + match e.kind() { + ErrorKind::NotFound | ErrorKind::ConnectionRefused => { + // This process claims itself as singleton as likely none exists + socket_cleanup(&socket); + listen_for_other_instances(socket, app.clone(), cb); + } + _ => { + tracing::debug!( + "single_instance failed to notify - launching normally: {}", + e + ); + } + } + } + } + Ok(()) + }) + .on_event(|app, event| { + if let RunEvent::Exit = event { + destroy(app); + } + }) + .build() +} + +pub fn destroy>(manager: &M) { + let socket = socket_path(manager.config(), manager.package_info()); + socket_cleanup(&socket); +} + +fn socket_path(config: &Config, _package_info: &tauri::PackageInfo) -> PathBuf { + let identifier = config.identifier.replace(['.', '-'].as_ref(), "_"); + + #[cfg(feature = "semver")] + let identifier = format!( + "{identifier}_{}", + semver_compat_string(&_package_info.version), + ); + + // Use /tmp as socket path must be shorter than 100 chars. + PathBuf::from(format!("/tmp/{}_si.sock", identifier)) +} + +fn socket_cleanup(socket: &PathBuf) { + let _ = std::fs::remove_file(socket); +} + +fn notify_singleton(socket: &PathBuf) -> Result<(), Error> { + let stream = UnixStream::connect(socket)?; + let mut bf = BufWriter::new(&stream); + let cwd = std::env::current_dir() + .unwrap_or_default() + .to_str() + .unwrap_or_default() + .to_string(); + bf.write_all(cwd.as_bytes())?; + bf.write_all(b"\0\0")?; + let args_joined = std::env::args().collect::>().join("\0"); + bf.write_all(args_joined.as_bytes())?; + bf.flush()?; + drop(bf); + Ok(()) +} + +fn listen_for_other_instances( + socket: PathBuf, + app: AppHandle, + mut cb: Box>, +) { + tauri::async_runtime::spawn(async move { + match tokio::net::UnixListener::bind(socket) { + Ok(listener) => loop { + match listener.accept().await { + Ok((mut stream, _addr)) => { + let mut s = String::new(); + match stream.read_to_string(&mut s).await { + Ok(_) => { + let (cwd, args) = s.split_once("\0\0").unwrap_or_default(); + let args: Vec = + args.split('\0').map(String::from).collect(); + cb(app.app_handle(), args, cwd.to_string()); + } + Err(e) => { + tracing::debug!("single_instance failed to be notified: {e}") + } + } + } + Err(err) => { + tracing::debug!("single_instance failed to be notified: {}", err); + continue; + } + } + }, + Err(err) => { + tracing::error!( + "single_instance failed to listen to other processes - launching normally: {}", + err + ); + } + } + }); +} diff --git a/plugins/single-instance/src/platform_impl/windows.rs b/plugins/single-instance/src/platform_impl/windows.rs new file mode 100644 index 0000000..196f785 --- /dev/null +++ b/plugins/single-instance/src/platform_impl/windows.rs @@ -0,0 +1,267 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +#[cfg(feature = "semver")] +use crate::semver_compat::semver_compat_string; + +use crate::SingleInstanceCallback; +use std::ffi::CStr; +use tauri::{ + plugin::{self, TauriPlugin}, + AppHandle, Manager, RunEvent, Runtime, +}; +use windows_sys::Win32::{ + Foundation::{CloseHandle, GetLastError, ERROR_ALREADY_EXISTS, HWND, LPARAM, LRESULT, WPARAM}, + System::{ + DataExchange::COPYDATASTRUCT, + LibraryLoader::GetModuleHandleW, + Threading::{CreateMutexW, ReleaseMutex}, + }, + UI::WindowsAndMessaging::{ + self as w32wm, AllowSetForegroundWindow, CreateWindowExW, DefWindowProcW, DestroyWindow, + FindWindowW, GetWindowThreadProcessId, RegisterClassExW, SendMessageW, CREATESTRUCTW, + GWLP_USERDATA, GWL_STYLE, WINDOW_LONG_PTR_INDEX, WM_COPYDATA, WM_CREATE, WM_DESTROY, + WNDCLASSEXW, WS_EX_LAYERED, WS_EX_NOACTIVATE, WS_EX_TOOLWINDOW, WS_EX_TRANSPARENT, + WS_OVERLAPPED, WS_POPUP, WS_VISIBLE, + }, +}; + +const WMCOPYDATA_SINGLE_INSTANCE_DATA: usize = 1542; + +struct MutexHandle(isize); + +struct TargetWindowHandle(isize); + +struct UserData { + app: AppHandle, + callback: Box>, +} + +impl UserData { + unsafe fn from_hwnd_raw(hwnd: HWND) -> *mut Self { + GetWindowLongPtrW(hwnd, GWLP_USERDATA) as *mut Self + } + + unsafe fn from_hwnd<'a>(hwnd: HWND) -> &'a mut Self { + &mut *Self::from_hwnd_raw(hwnd) + } + + fn run_callback(&mut self, args: Vec, cwd: String) { + (self.callback)(&self.app, args, cwd) + } +} + +pub fn init(callback: Box>) -> TauriPlugin { + plugin::Builder::new("single-instance") + .setup(|app, _api| { + #[allow(unused_mut)] + let mut id = app.config().identifier.clone(); + #[cfg(feature = "semver")] + { + id.push('_'); + id.push_str(semver_compat_string(&app.package_info().version).as_str()); + } + + let class_name = encode_wide(format!("{id}-sic")); + let window_name = encode_wide(format!("{id}-siw")); + let mutex_name = encode_wide(format!("{id}-sim")); + + let hmutex = + unsafe { CreateMutexW(std::ptr::null(), true.into(), mutex_name.as_ptr()) }; + + if unsafe { GetLastError() } == ERROR_ALREADY_EXISTS { + unsafe { + let hwnd = FindWindowW(class_name.as_ptr(), window_name.as_ptr()); + + if !hwnd.is_null() { + // Windows lets us bring a window to the front, but not the first + // instance. Hand that right over before we exit, so focusing a window + // from the callback works. Windows takes it back if the user switches + // to another app in the meantime. + let mut pid = 0; + GetWindowThreadProcessId(hwnd, &mut pid); + if pid != 0 { + AllowSetForegroundWindow(pid); + } + + let cwd = std::env::current_dir().unwrap_or_default(); + let cwd = cwd.to_str().unwrap_or_default(); + + let args = std::env::args().collect::>().join("|"); + + let data = format!("{cwd}|{args}\0",); + + let bytes = data.as_bytes(); + let cds = COPYDATASTRUCT { + dwData: WMCOPYDATA_SINGLE_INSTANCE_DATA, + cbData: bytes.len() as _, + lpData: bytes.as_ptr() as _, + }; + + SendMessageW(hwnd, WM_COPYDATA, 0, &cds as *const _ as _); + + app.cleanup_before_exit(); + std::process::exit(0); + } + } + } else { + app.manage(MutexHandle(hmutex as _)); + + let userdata = UserData { + app: app.clone(), + callback, + }; + let userdata = Box::into_raw(Box::new(userdata)); + let hwnd = create_event_target_window::(&class_name, &window_name, userdata); + app.manage(TargetWindowHandle(hwnd as _)); + } + + Ok(()) + }) + .on_event(|app, event| { + if let RunEvent::Exit = event { + destroy(app); + } + }) + .build() +} + +pub fn destroy>(manager: &M) { + if let Some(hmutex) = manager.try_state::() { + unsafe { + ReleaseMutex(hmutex.0 as _); + CloseHandle(hmutex.0 as _); + } + } + if let Some(hwnd) = manager.try_state::() { + unsafe { DestroyWindow(hwnd.0 as _) }; + } +} + +unsafe extern "system" fn single_instance_window_proc( + hwnd: HWND, + msg: u32, + wparam: WPARAM, + lparam: LPARAM, +) -> LRESULT { + match msg { + WM_CREATE => { + let create_struct = &*(lparam as *const CREATESTRUCTW); + let userdata = create_struct.lpCreateParams as *const UserData; + SetWindowLongPtrW(hwnd, GWLP_USERDATA, userdata as _); + 0 + } + + WM_COPYDATA => { + let cds_ptr = lparam as *const COPYDATASTRUCT; + if (*cds_ptr).dwData == WMCOPYDATA_SINGLE_INSTANCE_DATA { + let userdata = UserData::::from_hwnd(hwnd); + + let data = CStr::from_ptr((*cds_ptr).lpData as _).to_string_lossy(); + let mut s = data.split('|'); + let cwd = s.next().unwrap(); + let args = s.map(|s| s.to_string()).collect(); + + userdata.run_callback(args, cwd.to_string()); + } + 1 + } + + WM_DESTROY => { + let userdata = UserData::::from_hwnd_raw(hwnd); + drop(Box::from_raw(userdata)); + 0 + } + _ => DefWindowProcW(hwnd, msg, wparam, lparam), + } +} + +fn create_event_target_window( + class_name: &[u16], + window_name: &[u16], + userdata: *const UserData, +) -> HWND { + unsafe { + let class = WNDCLASSEXW { + cbSize: std::mem::size_of::() as u32, + style: 0, + lpfnWndProc: Some(single_instance_window_proc::), + cbClsExtra: 0, + cbWndExtra: 0, + hInstance: GetModuleHandleW(std::ptr::null()), + hIcon: std::ptr::null_mut(), + hCursor: std::ptr::null_mut(), + hbrBackground: std::ptr::null_mut(), + lpszMenuName: std::ptr::null(), + lpszClassName: class_name.as_ptr(), + hIconSm: std::ptr::null_mut(), + }; + + RegisterClassExW(&class); + + let hwnd = CreateWindowExW( + WS_EX_NOACTIVATE + | WS_EX_TRANSPARENT + | WS_EX_LAYERED + // WS_EX_TOOLWINDOW prevents this window from ever showing up in the taskbar, which + // we want to avoid. If you remove this style, this window won't show up in the + // taskbar *initially*, but it can show up at some later point. This can sometimes + // happen on its own after several hours have passed, although this has proven + // difficult to reproduce. Alternatively, it can be manually triggered by killing + // `explorer.exe` and then starting the process back up. + // It is unclear why the bug is triggered by waiting for several hours. + | WS_EX_TOOLWINDOW, + class_name.as_ptr(), + window_name.as_ptr(), + WS_OVERLAPPED, + 0, + 0, + 0, + 0, + std::ptr::null_mut(), + std::ptr::null_mut(), + GetModuleHandleW(std::ptr::null()), + userdata as _, + ); + SetWindowLongPtrW( + hwnd, + GWL_STYLE, + // The window technically has to be visible to receive WM_PAINT messages (which are used + // for delivering events during resizes), but it isn't displayed to the user because of + // the LAYERED style. + (WS_VISIBLE | WS_POPUP) as isize, + ); + hwnd + } +} + +pub fn encode_wide(string: impl AsRef) -> Vec { + std::os::windows::prelude::OsStrExt::encode_wide(string.as_ref()) + .chain(std::iter::once(0)) + .collect() +} + +#[cfg(target_pointer_width = "32")] +#[allow(non_snake_case)] +unsafe fn SetWindowLongPtrW(hwnd: HWND, index: WINDOW_LONG_PTR_INDEX, value: isize) -> isize { + w32wm::SetWindowLongW(hwnd, index, value as _) as _ +} + +#[cfg(target_pointer_width = "64")] +#[allow(non_snake_case)] +unsafe fn SetWindowLongPtrW(hwnd: HWND, index: WINDOW_LONG_PTR_INDEX, value: isize) -> isize { + w32wm::SetWindowLongPtrW(hwnd, index, value) +} + +#[cfg(target_pointer_width = "32")] +#[allow(non_snake_case)] +unsafe fn GetWindowLongPtrW(hwnd: HWND, index: WINDOW_LONG_PTR_INDEX) -> isize { + w32wm::GetWindowLongW(hwnd, index) as _ +} + +#[cfg(target_pointer_width = "64")] +#[allow(non_snake_case)] +unsafe fn GetWindowLongPtrW(hwnd: HWND, index: WINDOW_LONG_PTR_INDEX) -> isize { + w32wm::GetWindowLongPtrW(hwnd, index) +} diff --git a/plugins/single-instance/src/semver_compat.rs b/plugins/single-instance/src/semver_compat.rs new file mode 100644 index 0000000..2920f1b --- /dev/null +++ b/plugins/single-instance/src/semver_compat.rs @@ -0,0 +1,19 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +/// Takes a version and spits out a String with trailing _x, thus only considering the digits +/// relevant regarding semver compatibility +pub fn semver_compat_string(version: &semver::Version) -> String { + // for pre-release always treat each version separately + if !version.pre.is_empty() { + return version.to_string().replace(['.', '-'], "_"); + } + match version.major { + 0 => match version.minor { + 0 => format!("0_0_{}", version.patch), + _ => format!("0_{}_x", version.minor), + }, + _ => format!("{}_x_x", version.major), + } +} diff --git a/plugins/sql/CHANGELOG.md b/plugins/sql/CHANGELOG.md new file mode 100644 index 0000000..35c93c5 --- /dev/null +++ b/plugins/sql/CHANGELOG.md @@ -0,0 +1,142 @@ +# Changelog + +## [2.4.1] + +- [`f8053e65`](https://github.com/tauri-apps/plugins-workspace/commit/f8053e659e4ccd85c1f52833411ff8417cbc5e69) ([#3527](https://github.com/tauri-apps/plugins-workspace/pull/3527) by [@Legend-Master](https://github.com/tauri-apps/plugins-workspace/../../Legend-Master)) Documented Cargo feature flags in each plugin's crate-level documentation. + +## \[2.4.0] + +- [`1dc36128`](https://github.com/tauri-apps/plugins-workspace/commit/1dc36128624f6954e09ea1574f68615e34e83f63) ([#3275](https://github.com/tauri-apps/plugins-workspace/pull/3275) by [@mikenikles](https://github.com/tauri-apps/plugins-workspace/../../mikenikles)) Add support for Postgres `NUMERIC` and custom data types. + +## \[2.3.2] + +- [`2dc3f3f0`](https://github.com/tauri-apps/plugins-workspace/commit/2dc3f3f0397fa91b1f3ffbc4b42ca2b4ae55bbe0) ([#3144](https://github.com/tauri-apps/plugins-workspace/pull/3144)) Fixes issue with UUIDs returned as null in sql query results. + +## \[2.3.1] + +- [`93426f85`](https://github.com/tauri-apps/plugins-workspace/commit/93426f85120f49beb9f40222bff45185a32d54a9) Fixed an issue that caused docs.rs builds to fail. No user facing changes. + +## \[2.3.0] + +- [`f209b2f2`](https://github.com/tauri-apps/plugins-workspace/commit/f209b2f23cb29133c97ad5961fb46ef794dbe063) ([#2804](https://github.com/tauri-apps/plugins-workspace/pull/2804) by [@renovate](https://github.com/tauri-apps/plugins-workspace/../../renovate)) Updated tauri to 2.6 + +## \[2.2.1] + +- [`f634e524`](https://github.com/tauri-apps/plugins-workspace/commit/f634e5248ebe428f8305a59f74c13fc15147fb8e) This is an "empty" release to update the plugins' source files on crates.io and docs.rs. This should fix docs.rs build failures for projects using tauri plugins as dependencies. + +## \[2.2.0] + +- [`3a79266b`](https://github.com/tauri-apps/plugins-workspace/commit/3a79266b8cf96a55b1ae6339d725567d45a44b1d) ([#2173](https://github.com/tauri-apps/plugins-workspace/pull/2173) by [@FabianLars](https://github.com/tauri-apps/plugins-workspace/../../FabianLars)) Bumped all plugins to `v2.2.0`. From now, the versions for the Rust and JavaScript packages of each plugin will be in sync with each other. + +## \[2.0.2] + +- [`31956469`](https://github.com/tauri-apps/plugins-workspace/commit/319564699638c080b73d506bcaad186ecc4a8236) ([#1928](https://github.com/tauri-apps/plugins-workspace/pull/1928) by [@FabianLars](https://github.com/tauri-apps/plugins-workspace/../../FabianLars)) Fixed the QueryResult typing by marking `lastInsertId` as optional to reflect postgres-only changes made in the 2.0.0 release. + +## \[2.0.3] + +- [`90ef77c8`](https://github.com/tauri-apps/plugins-workspace/commit/90ef77c8723ac9d0ba7bd3b52a80a2b14843ff99) ([#2038](https://github.com/tauri-apps/plugins-workspace/pull/2038) by [@johncarmack1984](https://github.com/tauri-apps/plugins-workspace/../../johncarmack1984)) Allow blocking on async code without creating a nested runtime. + +## \[2.0.1] + +- [`0ca4cc91`](https://github.com/tauri-apps/plugins-workspace/commit/0ca4cc914c5ea995c98f9e60a2ab49827c219350) ([#1963](https://github.com/tauri-apps/plugins-workspace/pull/1963) by [@yoggys](https://github.com/tauri-apps/plugins-workspace/../../yoggys)) Fixed incorrect documentation of the select method in the Database class. + +## \[2.0.1] + +- [`a1a82208`](https://github.com/tauri-apps/plugins-workspace/commit/a1a82208ed4ab87f83310be0dc95428aec9ab241) ([#1873](https://github.com/tauri-apps/plugins-workspace/pull/1873) by [@lucasfernog](https://github.com/tauri-apps/plugins-workspace/../../lucasfernog)) Downgrade MSRV to 1.77.2 to support Windows 7. + +## \[2.0.0] + +- [`e2c4dfb6`](https://github.com/tauri-apps/plugins-workspace/commit/e2c4dfb6af43e5dd8d9ceba232c315f5febd55c1) Update to tauri v2 stable release. + +## \[2.0.0-rc.3] + +- [`30bcf5dc`](https://github.com/tauri-apps/plugins-workspace/commit/30bcf5dcc22e1bb1fb983a8d2887edc39404e6df) ([#1838](https://github.com/tauri-apps/plugins-workspace/pull/1838) by [@FabianLars](https://github.com/tauri-apps/plugins-workspace/../../FabianLars)) It is now possible to enable multiple SQL backends at the same time. There will be no compile error anymore if no backends are enabled! + +## \[2.0.0-rc.2] + +- [`0dd97d91`](https://github.com/tauri-apps/plugins-workspace/commit/0dd97d911569cdedab07f504b708036d62ff83c1) ([#1375](https://github.com/tauri-apps/plugins-workspace/pull/1375) by [@KauanCurbani](https://github.com/tauri-apps/plugins-workspace/../../KauanCurbani)) Added support for `UUID` columns to the postgres implementation. + +## \[2.0.0-rc.1] + +- [`e2e97db5`](https://github.com/tauri-apps/plugins-workspace/commit/e2e97db51983267f5be84d4f6f0278d58834d1f5) ([#1701](https://github.com/tauri-apps/plugins-workspace/pull/1701) by [@lucasfernog](https://github.com/tauri-apps/plugins-workspace/../../lucasfernog)) Update to tauri 2.0.0-rc.8 + +## \[2.0.0-rc.0] + +- [`9887d1`](https://github.com/tauri-apps/plugins-workspace/commit/9887d14bd0e971c4c0f5c1188fc4005d3fc2e29e) Update to tauri RC. + +## \[2.0.0-beta.8] + +- [`99d6ac0f`](https://github.com/tauri-apps/plugins-workspace/commit/99d6ac0f9506a6a4a1aa59c728157190a7441af6) ([#1606](https://github.com/tauri-apps/plugins-workspace/pull/1606) by [@FabianLars](https://github.com/tauri-apps/plugins-workspace/../../FabianLars)) The JS packages now specify the *minimum* `@tauri-apps/api` version instead of a single exact version. +- [`6de87966`](https://github.com/tauri-apps/plugins-workspace/commit/6de87966ecc00ad9d91c25be452f1f46bd2b7e1f) ([#1597](https://github.com/tauri-apps/plugins-workspace/pull/1597) by [@Legend-Master](https://github.com/tauri-apps/plugins-workspace/../../Legend-Master)) Update to tauri beta.25. + +## \[2.0.0-beta.10] + +- [`37cb9a66`](https://github.com/tauri-apps/plugins-workspace/commit/37cb9a6681b948908cd9443340f6b23401607df7) ([#1575](https://github.com/tauri-apps/plugins-workspace/pull/1575) by [@renovate](https://github.com/tauri-apps/plugins-workspace/../../renovate)) Update sqlx to 0.8 - Check out their changelog for behavior changes: https://github.com/launchbadge/sqlx/blob/main/CHANGELOG.md#080---2024-07-22 + +## \[2.0.0-beta.7] + +- [`22a17980`](https://github.com/tauri-apps/plugins-workspace/commit/22a17980ff4f6f8c40adb1b8f4ffc6dae2fe7e30) ([#1537](https://github.com/tauri-apps/plugins-workspace/pull/1537) by [@lucasfernog](https://github.com/tauri-apps/plugins-workspace/../../lucasfernog)) Update to tauri beta.24. + +## \[2.0.0-beta.6] + +- [`76daee7a`](https://github.com/tauri-apps/plugins-workspace/commit/76daee7aafece34de3092c86e531cf9eb1138989) ([#1512](https://github.com/tauri-apps/plugins-workspace/pull/1512) by [@renovate](https://github.com/tauri-apps/plugins-workspace/../../renovate)) Update to tauri beta.23. + +## \[2.0.0-beta.7] + +- [`4216c051`](https://github.com/tauri-apps/plugins-workspace/commit/4216c0517fd1dcb29d0162dc2fc15291472a2b00) ([#1381](https://github.com/tauri-apps/plugins-workspace/pull/1381) by [@thewh1teagle](https://github.com/tauri-apps/plugins-workspace/../../thewh1teagle)) Made `DbInstances` public for managing database instances directly from `Rust`. + +## \[2.0.0-beta.5] + +- [`9013854f`](https://github.com/tauri-apps/plugins-workspace/commit/9013854f42a49a230b9dbb9d02774765528a923f)([#1382](https://github.com/tauri-apps/plugins-workspace/pull/1382)) Update to tauri beta.22. + +## \[2.0.0-beta.4] + +- [`430bd6f4`](https://github.com/tauri-apps/plugins-workspace/commit/430bd6f4f379bee5d232ae6b098ae131db7f178a)([#1363](https://github.com/tauri-apps/plugins-workspace/pull/1363)) Update to tauri beta.20. + +## \[2.0.0-beta.3] + +- [`bd1ed590`](https://github.com/tauri-apps/plugins-workspace/commit/bd1ed5903ffcce5500310dac1e59e8c67674ef1e)([#1237](https://github.com/tauri-apps/plugins-workspace/pull/1237)) Update to tauri beta.17. + +## \[2.0.0-beta.3] + +- [`a04ea2f`](https://github.com/tauri-apps/plugins-workspace/commit/a04ea2f38294d5a3987578283badc8eec87a7752)([#1071](https://github.com/tauri-apps/plugins-workspace/pull/1071)) The global API script is now only added to the binary when the `withGlobalTauri` config is true. + +## \[2.0.0-beta.2] + +- [`99bea25`](https://github.com/tauri-apps/plugins-workspace/commit/99bea2559c2c0648c2519c50a18cd124dacef57b)([#1005](https://github.com/tauri-apps/plugins-workspace/pull/1005)) Update to tauri beta.8. + +## \[2.0.0-beta.1] + +- [`569defb`](https://github.com/tauri-apps/plugins-workspace/commit/569defbe9492e38938554bb7bdc1be9151456d21) Update to tauri beta.4. + +## \[2.0.0-beta.0] + +- [`d198c01`](https://github.com/tauri-apps/plugins-workspace/commit/d198c014863ee260cb0de88a14b7fc4356ef7474)([#862](https://github.com/tauri-apps/plugins-workspace/pull/862)) Update to tauri beta. + +## \[2.0.0-alpha.5] + +- [`387c2f9`](https://github.com/tauri-apps/plugins-workspace/commit/387c2f9e0ce4c75c07ffa3fd76391a25b58f5daf)([#802](https://github.com/tauri-apps/plugins-workspace/pull/802)) Update to @tauri-apps/api v2.0.0-alpha.13. + +## \[2.0.0-alpha.4] + +- [`387c2f9`](https://github.com/tauri-apps/plugins-workspace/commit/387c2f9e0ce4c75c07ffa3fd76391a25b58f5daf)([#802](https://github.com/tauri-apps/plugins-workspace/pull/802)) Update to @tauri-apps/api v2.0.0-alpha.12. + +## \[2.0.0-alpha.3] + +- [`e438e0a`](https://github.com/tauri-apps/plugins-workspace/commit/e438e0a62d4b430a5159f05f13ecd397dd891a0d)([#676](https://github.com/tauri-apps/plugins-workspace/pull/676)) Update to @tauri-apps/api v2.0.0-alpha.11. + +## \[2.0.0-alpha.2] + +- [`5c13736`](https://github.com/tauri-apps/plugins-workspace/commit/5c137365c60790e8d4037d449e8237aa3fffdab0)([#673](https://github.com/tauri-apps/plugins-workspace/pull/673)) Update to @tauri-apps/api v2.0.0-alpha.9. + +## \[2.0.0-alpha.2] + +- [`4e2cef9`](https://github.com/tauri-apps/plugins-workspace/commit/4e2cef9b702bbbb9cf4ee17de50791cb21f1b2a4)([#593](https://github.com/tauri-apps/plugins-workspace/pull/593)) Update to alpha.12. + +## \[2.0.0-alpha.1] + +- [`d74fc0a`](https://github.com/tauri-apps/plugins-workspace/commit/d74fc0a097996e90a37be8f57d50b7d1f6ca616f)([#555](https://github.com/tauri-apps/plugins-workspace/pull/555)) Update to alpha.11. + +## \[2.0.0-alpha.0] + +- [`717ae67`](https://github.com/tauri-apps/plugins-workspace/commit/717ae670978feb4492fac1f295998b93f2b9347f)([#371](https://github.com/tauri-apps/plugins-workspace/pull/371)) First v2 alpha release! diff --git a/plugins/sql/Cargo.toml b/plugins/sql/Cargo.toml new file mode 100644 index 0000000..4cb324a --- /dev/null +++ b/plugins/sql/Cargo.toml @@ -0,0 +1,50 @@ +[package] +name = "tauri-plugin-sql" +version = "2.4.1" +description = "Interface with SQL databases." +authors = { workspace = true } +license = { workspace = true } +edition = { workspace = true } +rust-version = { workspace = true } +repository = { workspace = true } +links = "tauri-plugin-sql" +exclude = [ + "/banner.png", + "/guest-js", + "/package.json", + "/rollup.config.js", + "/tsconfig.json", +] + +[package.metadata.docs.rs] +features = ["sqlite"] + +[package.metadata.platforms.support] +windows = { level = "full", notes = "" } +linux = { level = "full", notes = "" } +macos = { level = "full", notes = "" } +android = { level = "full", notes = "" } +ios = { level = "full", notes = "" } + +[build-dependencies] +tauri-plugin = { workspace = true, features = ["build"] } + +[dependencies] +serde = { workspace = true } +serde_json = { workspace = true } +tauri = { workspace = true } +log = { workspace = true } +thiserror = { workspace = true } +futures-core = "0.3" +sqlx = { version = "0.8", features = ["json", "time", "uuid", "rust_decimal"] } +rust_decimal = "1" +time = "0.3" +tokio = { version = "1", features = ["sync"] } +indexmap = { version = "2", features = ["serde"] } +uuid = "1" + +[features] +sqlite = ["sqlx/sqlite", "sqlx/runtime-tokio"] +mysql = ["sqlx/mysql", "sqlx/runtime-tokio-rustls"] +postgres = ["sqlx/postgres", "sqlx/runtime-tokio-rustls"] +# TODO: bundled-cipher etc diff --git a/plugins/sql/LICENSE.spdx b/plugins/sql/LICENSE.spdx new file mode 100644 index 0000000..cdd0df5 --- /dev/null +++ b/plugins/sql/LICENSE.spdx @@ -0,0 +1,20 @@ +SPDXVersion: SPDX-2.1 +DataLicense: CC0-1.0 +PackageName: tauri +DataFormat: SPDXRef-1 +PackageSupplier: Organization: The Tauri Programme in the Commons Conservancy +PackageHomePage: https://tauri.app +PackageLicenseDeclared: Apache-2.0 +PackageLicenseDeclared: MIT +PackageCopyrightText: 2019-2022, The Tauri Programme in the Commons Conservancy +PackageSummary: Tauri is a rust project that enables developers to make secure +and small desktop applications using a web frontend. + +PackageComment: The package includes the following libraries; see +Relationship information. + +Created: 2019-05-20T09:00:00Z +PackageDownloadLocation: git://github.com/tauri-apps/tauri +PackageDownloadLocation: git+https://github.com/tauri-apps/tauri.git +PackageDownloadLocation: git+ssh://github.com/tauri-apps/tauri.git +Creator: Person: Daniel Thompson-Yvetot \ No newline at end of file diff --git a/plugins/sql/LICENSE_APACHE-2.0 b/plugins/sql/LICENSE_APACHE-2.0 new file mode 100644 index 0000000..4947287 --- /dev/null +++ b/plugins/sql/LICENSE_APACHE-2.0 @@ -0,0 +1,177 @@ + + 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 \ No newline at end of file diff --git a/plugins/sql/LICENSE_MIT b/plugins/sql/LICENSE_MIT new file mode 100644 index 0000000..4d75472 --- /dev/null +++ b/plugins/sql/LICENSE_MIT @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2017 - Present Tauri Apps 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. \ No newline at end of file diff --git a/plugins/sql/README.md b/plugins/sql/README.md new file mode 100644 index 0000000..2e1e4ca --- /dev/null +++ b/plugins/sql/README.md @@ -0,0 +1,211 @@ +![plugin-sql](https://github.com/tauri-apps/plugins-workspace/raw/v2/plugins/sql/banner.png) + +Interface with SQL databases through [sqlx](https://github.com/launchbadge/sqlx). It supports the `sqlite`, `mysql` and `postgres` drivers, enabled by a Cargo feature. + +| Platform | Supported | +| -------- | --------- | +| Linux | ✓ | +| Windows | ✓ | +| macOS | ✓ | +| Android | ✓ | +| iOS | x | + +## Install + +_This plugin requires a Rust version of at least **1.77.2**_ + +There are three general methods of installation that we can recommend. + +1. Use crates.io and npm (easiest, and requires you to trust that our publishing pipeline worked) +2. Pull sources directly from Github using git tags / revision hashes (most secure) +3. Git submodule install this repo in your tauri project and then use file protocol to ingest the source (most secure, but inconvenient to use) + +Install the Core plugin by adding the following to your `Cargo.toml` file: + +`src-tauri/Cargo.toml` + +```toml +[dependencies.tauri-plugin-sql] +features = ["sqlite"] # or "postgres", or "mysql" +version = "2.0.0" +# alternatively with Git +git = "https://github.com/tauri-apps/plugins-workspace" +branch = "v2" +``` + +You can install the JavaScript Guest bindings using your preferred JavaScript package manager: + +```sh +pnpm add @tauri-apps/plugin-sql +# or +npm add @tauri-apps/plugin-sql +# or +yarn add @tauri-apps/plugin-sql +``` + +## Usage + +First you need to register the core plugin with Tauri: + +`src-tauri/src/lib.rs` + +```rust +fn main() { + tauri::Builder::default() + .plugin(tauri_plugin_sql::Builder::default().build()) + .run(tauri::generate_context!()) + .expect("error while running tauri application"); +} +``` + +Afterwards all the plugin's APIs are available through the JavaScript guest bindings: + +```javascript +import Database from '@tauri-apps/plugin-sql' + +// sqlite. The path is relative to `tauri::api::path::BaseDirectory::AppConfig`. +const db = await Database.load('sqlite:test.db') +// mysql +const db = await Database.load('mysql://user:pass@host/database') +// postgres +const db = await Database.load('postgres://postgres:password@localhost/test') + +await db.execute('INSERT INTO ...') +``` + +## Syntax + +We use sqlx as our underlying library, adopting their query syntax: + +- sqlite and postgres use the "$#" syntax when substituting query data +- mysql uses "?" when substituting query data + +```javascript +// INSERT and UPDATE examples for sqlite and postgres +const result = await db.execute( + 'INSERT into todos (id, title, status) VALUES ($1, $2, $3)', + [todos.id, todos.title, todos.status] +) + +const result = await db.execute( + 'UPDATE todos SET title = $1, status = $2 WHERE id = $3', + [todos.title, todos.status, todos.id] +) + +// INSERT and UPDATE examples for mysql +const result = await db.execute( + 'INSERT into todos (id, title, status) VALUES (?, ?, ?)', + [todos.id, todos.title, todos.status] +) + +const result = await db.execute( + 'UPDATE todos SET title = ?, status = ? WHERE id = ?', + [todos.title, todos.status, todos.id] +) +``` + +## Migrations + +This plugin supports database migrations, allowing you to manage database schema evolution over time. + +### Defining Migrations + +Migrations are defined in Rust using the `Migration` struct. Each migration should include a unique version number, a description, the SQL to be executed, and the type of migration (Up or Down). + +Example of a migration: + +```rust +use tauri_plugin_sql::{Migration, MigrationKind}; + +let migration = Migration { + version: 1, + description: "create_initial_tables", + sql: "CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT);", + kind: MigrationKind::Up, +}; +``` + +### Adding Migrations to the Plugin Builder + +Migrations are registered with the `Builder` struct provided by the plugin. Use the `add_migrations` method to add your migrations to the plugin for a specific database connection. + +Example of adding migrations: + +```rust +use tauri_plugin_sql::{Builder, Migration, MigrationKind}; + +fn main() { + let migrations = vec![ + // Define your migrations here + Migration { + version: 1, + description: "create_initial_tables", + sql: "CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT);", + kind: MigrationKind::Up, + } + ]; + + tauri::Builder::default() + .plugin( + tauri_plugin_sql::Builder::default() + .add_migrations("sqlite:mydatabase.db", migrations) + .build(), + ) + ... +} +``` + +### Applying Migrations + +To apply the migrations when the plugin is initialized, add the connection string to the `tauri.conf.json` file: + +```json +{ + "plugins": { + "sql": { + "preload": ["sqlite:mydatabase.db"] + } + } +} +``` + +Alternatively, the client side `load()` also runs the migrations for a given connection string: + +```ts +import Database from '@tauri-apps/plugin-sql' +const db = await Database.load('sqlite:mydatabase.db') +``` + +Ensure that the migrations are defined in the correct order and are safe to run multiple times. + +### Migration Management + +- **Version Control**: Each migration must have a unique version number. This is crucial for ensuring the migrations are applied in the correct order. +- **Idempotency**: Write migrations in a way that they can be safely re-run without causing errors or unintended consequences. +- **Testing**: Thoroughly test migrations to ensure they work as expected and do not compromise the integrity of your database. + +## Contributing + +PRs accepted. Please make sure to read the Contributing Guide before making a pull request. + +## Partners + + + + + + + +
+ + CrabNebula + +
+ +For the complete list of sponsors please visit our [website](https://tauri.app#sponsors) and [Open Collective](https://opencollective.com/tauri). + +## License + +Code: (c) 2015 - Present - The Tauri Programme within The Commons Conservancy. + +MIT or MIT/Apache 2.0 where applicable. diff --git a/plugins/sql/SECURITY.md b/plugins/sql/SECURITY.md new file mode 100644 index 0000000..4f09bba --- /dev/null +++ b/plugins/sql/SECURITY.md @@ -0,0 +1,23 @@ +# Security Policy + +**Do not report security vulnerabilities through public GitHub issues.** + +**Please use the [Private Vulnerability Disclosure](https://docs.github.com/en/code-security/security-advisories/guidance-on-reporting-and-writing-information-about-vulnerabilities/privately-reporting-a-security-vulnerability#privately-reporting-a-security-vulnerability) feature of GitHub.** + +Include as much of the following information: + +- Type of issue (e.g. improper input parsing, privilege escalation, etc.) +- The location of the affected source code (tag/branch/commit or direct URL) +- Any special configuration required to reproduce the issue +- The distribution affected or used to help us with reproduction of the issue +- Step-by-step instructions to reproduce the issue +- Ideally a reproduction repository +- Impact of the issue, including how an attacker might exploit the issue + +We prefer to receive reports in English. + +## Contact + +Please disclose a vulnerability or security relevant issue here: [https://github.com/tauri-apps/plugins-workspace/security/advisories/new](https://github.com/tauri-apps/plugins-workspace/security/advisories/new). + +Alternatively, you can also contact us by email via [security@tauri.app](mailto:security@tauri.app). diff --git a/plugins/sql/api-iife.js b/plugins/sql/api-iife.js new file mode 100644 index 0000000..036d8ee --- /dev/null +++ b/plugins/sql/api-iife.js @@ -0,0 +1 @@ +if("__TAURI__"in window){var __TAURI_PLUGIN_SQL__=function(e){"use strict";class t{constructor(e){this.path=e}static async load(n){const s=await e.invoke("plugin:sql|load",{db:n});return new t(s)}static get(e){return new t(e)}async execute(t,n){const[s,a]=await e.invoke("plugin:sql|execute",{db:this.path,query:t,values:n??[]});return{lastInsertId:a,rowsAffected:s}}async select(t,n){return await e.invoke("plugin:sql|select",{db:this.path,query:t,values:n??[]})}async close(t){return await e.invoke("plugin:sql|close",{db:t})}}return t}(window.__TAURI__.core);Object.defineProperty(window.__TAURI__,"sql",{value:__TAURI_PLUGIN_SQL__})} diff --git a/plugins/sql/banner.png b/plugins/sql/banner.png new file mode 100644 index 0000000000000000000000000000000000000000..5e53a31b40da2334134098b907c703dc65a48d29 GIT binary patch literal 40282 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 F_doq!015yA literal 0 HcmV?d00001 diff --git a/plugins/sql/build.rs b/plugins/sql/build.rs new file mode 100644 index 0000000..fbbca42 --- /dev/null +++ b/plugins/sql/build.rs @@ -0,0 +1,11 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +const COMMANDS: &[&str] = &["load", "execute", "select", "close"]; + +fn main() { + tauri_plugin::Builder::new(COMMANDS) + .global_api_script_path("./api-iife.js") + .build(); +} diff --git a/plugins/sql/guest-js/index.ts b/plugins/sql/guest-js/index.ts new file mode 100644 index 0000000..19e40af --- /dev/null +++ b/plugins/sql/guest-js/index.ts @@ -0,0 +1,226 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +/** + * Interface with SQL databases through [sqlx](https://github.com/launchbadge/sqlx). + * Which database engines can be used depends on the drivers enabled on the Rust + * side of the plugin: SQLite, MySQL and PostgreSQL. + * + * @module + */ + +import { invoke } from '@tauri-apps/api/core' + +/** + * The outcome of a statement run through {@link Database.execute}. + */ +export interface QueryResult { + /** The number of rows affected by the query. */ + rowsAffected: number + /** + * The last inserted `id`. + * + * This value is not set for Postgres databases. If the + * last inserted id is required on Postgres, the `select` function + * must be used, with a `RETURNING` clause + * (`INSERT INTO todos (title) VALUES ($1) RETURNING id`). + */ + lastInsertId?: number +} + +/** + * **Database** + * + * The `Database` class serves as the primary interface for + * communicating with the rust side of the sql plugin. + * + * @since 2.0.0 + */ +export default class Database { + /** + * The connection string identifying the database on the Rust side, + * for instance `sqlite:test.db`, `mysql://user:pass@host/database` + * or `postgres://user:pass@host/database`. + */ + path: string + + /** + * Creates a `Database` instance for the given connection string without + * opening a connection to it. Use {@link Database.load} to connect to the + * database, or {@link Database.get} for a database that is already loaded. + * + * @param path The database connection string, such as `sqlite:test.db`. + * + * @example + * ```typescript + * import Database from '@tauri-apps/plugin-sql' + * const db = new Database('sqlite:test.db') + * ``` + */ + constructor(path: string) { + this.path = path + } + + /** + * **load** + * + * A static initializer which connects to the underlying database and + * returns a `Database` instance once a connection to the database is established. + * + * # Sqlite + * + * The path is relative to `tauri::path::BaseDirectory::App` and must start with `sqlite:`. + * + * @example + * ```typescript + * import Database from '@tauri-apps/plugin-sql' + * const db = await Database.load('sqlite:test.db') + * ``` + * + * @param path The database connection string, such as `sqlite:test.db`. The database is created if it does not exist yet, and any migration registered for it on the Rust side is run. + * @returns A promise resolving to a `Database` instance connected to the given database. + */ + static async load(path: string): Promise { + const _path = await invoke('plugin:sql|load', { + db: path + }) + + return new Database(_path) + } + + /** + * **get** + * + * A static initializer which synchronously returns an instance of + * the Database class while deferring the actual database connection + * until the first invocation or selection on the database. + * + * # Sqlite + * + * The path is relative to `tauri::path::BaseDirectory::App` and must start with `sqlite:`. + * + * @example + * ```typescript + * import Database from '@tauri-apps/plugin-sql' + * const db = Database.get('sqlite:test.db') + * ``` + * + * @param path The database connection string, such as `sqlite:test.db`. + * @returns A `Database` instance bound to the given connection string. + */ + static get(path: string): Database { + return new Database(path) + } + + /** + * **execute** + * + * Passes a SQL expression to the database for execution. + * + * @example + * ```typescript + * import Database from '@tauri-apps/plugin-sql' + * const db = await Database.load('sqlite:test.db') + * + * // for sqlite & postgres + * // INSERT example + * const result = await db.execute( + * "INSERT into todos (id, title, status) VALUES ($1, $2, $3)", + * [ todos.id, todos.title, todos.status ] + * ); + * // UPDATE example + * const result = await db.execute( + * "UPDATE todos SET title = $1, completed = $2 WHERE id = $3", + * [ todos.title, todos.status, todos.id ] + * ); + * + * // for mysql + * // INSERT example + * const result = await db.execute( + * "INSERT into todos (id, title, status) VALUES (?, ?, ?)", + * [ todos.id, todos.title, todos.status ] + * ); + * // UPDATE example + * const result = await db.execute( + * "UPDATE todos SET title = ?, completed = ? WHERE id = ?", + * [ todos.title, todos.status, todos.id ] + * ); + * ``` + * + * @param query The SQL statement to run, using `$1`, `$2`, ... placeholders on SQLite and PostgreSQL and `?` placeholders on MySQL. + * @param bindValues The values bound to the query placeholders, in the order they appear in the statement. Defaults to no values. + * @returns A promise resolving to the number of rows affected by the statement and the last inserted id. + */ + async execute(query: string, bindValues?: unknown[]): Promise { + const [rowsAffected, lastInsertId] = await invoke<[number, number]>( + 'plugin:sql|execute', + { + db: this.path, + query, + values: bindValues ?? [] + } + ) + return { + lastInsertId, + rowsAffected + } + } + + /** + * **select** + * + * Passes in a SELECT query to the database for execution. + * + * @example + * ```typescript + * import Database from '@tauri-apps/plugin-sql' + * const db = await Database.load('sqlite:test.db') + * + * // for sqlite & postgres + * const result = await db.select( + * "SELECT * from todos WHERE id = $1", [ id ] + * ); + * + * // for mysql + * const result = await db.select( + * "SELECT * from todos WHERE id = ?", [ id ] + * ); + * ``` + * + * @param query The SQL query to run, using `$1`, `$2`, ... placeholders on SQLite and PostgreSQL and `?` placeholders on MySQL. + * @param bindValues The values bound to the query placeholders, in the order they appear in the query. Defaults to no values. + * @returns A promise resolving to the selected rows, each row being an object keyed by column name. + */ + async select(query: string, bindValues?: unknown[]): Promise { + const result = await invoke('plugin:sql|select', { + db: this.path, + query, + values: bindValues ?? [] + }) + + return result + } + + /** + * **close** + * + * Closes the database connection pool. + * + * @example + * ```typescript + * import Database from '@tauri-apps/plugin-sql' + * const db = await Database.load('sqlite:test.db') + * const success = await db.close() + * ``` + * + * @param db - Optionally state the name of a database if you are managing more than one. Otherwise, all database pools will be in scope. + * @returns A promise resolving to `true` once the matching connection pools have been closed. + */ + async close(db?: string): Promise { + const success = await invoke('plugin:sql|close', { + db + }) + return success + } +} diff --git a/plugins/sql/package.json b/plugins/sql/package.json new file mode 100644 index 0000000..b6eb93f --- /dev/null +++ b/plugins/sql/package.json @@ -0,0 +1,30 @@ +{ + "name": "@tauri-apps/plugin-sql", + "version": "2.4.1", + "description": "Interface with SQL databases", + "license": "MIT OR Apache-2.0", + "authors": [ + "Tauri Programme within The Commons Conservancy" + ], + "repository": "https://github.com/tauri-apps/plugins-workspace", + "type": "module", + "types": "./dist-js/index.d.ts", + "main": "./dist-js/index.cjs", + "module": "./dist-js/index.js", + "exports": { + "types": "./dist-js/index.d.ts", + "import": "./dist-js/index.js", + "require": "./dist-js/index.cjs" + }, + "scripts": { + "build": "rollup -c" + }, + "files": [ + "dist-js", + "README.md", + "LICENSE" + ], + "dependencies": { + "@tauri-apps/api": "^2.11.0" + } +} diff --git a/plugins/sql/permissions/autogenerated/commands/close.toml b/plugins/sql/permissions/autogenerated/commands/close.toml new file mode 100644 index 0000000..fad12d1 --- /dev/null +++ b/plugins/sql/permissions/autogenerated/commands/close.toml @@ -0,0 +1,13 @@ +# Automatically generated - DO NOT EDIT! + +"$schema" = "../../schemas/schema.json" + +[[permission]] +identifier = "allow-close" +description = "Enables the close command without any pre-configured scope." +commands.allow = ["close"] + +[[permission]] +identifier = "deny-close" +description = "Denies the close command without any pre-configured scope." +commands.deny = ["close"] diff --git a/plugins/sql/permissions/autogenerated/commands/execute.toml b/plugins/sql/permissions/autogenerated/commands/execute.toml new file mode 100644 index 0000000..d98be89 --- /dev/null +++ b/plugins/sql/permissions/autogenerated/commands/execute.toml @@ -0,0 +1,13 @@ +# Automatically generated - DO NOT EDIT! + +"$schema" = "../../schemas/schema.json" + +[[permission]] +identifier = "allow-execute" +description = "Enables the execute command without any pre-configured scope." +commands.allow = ["execute"] + +[[permission]] +identifier = "deny-execute" +description = "Denies the execute command without any pre-configured scope." +commands.deny = ["execute"] diff --git a/plugins/sql/permissions/autogenerated/commands/load.toml b/plugins/sql/permissions/autogenerated/commands/load.toml new file mode 100644 index 0000000..f6e47ad --- /dev/null +++ b/plugins/sql/permissions/autogenerated/commands/load.toml @@ -0,0 +1,13 @@ +# Automatically generated - DO NOT EDIT! + +"$schema" = "../../schemas/schema.json" + +[[permission]] +identifier = "allow-load" +description = "Enables the load command without any pre-configured scope." +commands.allow = ["load"] + +[[permission]] +identifier = "deny-load" +description = "Denies the load command without any pre-configured scope." +commands.deny = ["load"] diff --git a/plugins/sql/permissions/autogenerated/commands/select.toml b/plugins/sql/permissions/autogenerated/commands/select.toml new file mode 100644 index 0000000..5a13a02 --- /dev/null +++ b/plugins/sql/permissions/autogenerated/commands/select.toml @@ -0,0 +1,13 @@ +# Automatically generated - DO NOT EDIT! + +"$schema" = "../../schemas/schema.json" + +[[permission]] +identifier = "allow-select" +description = "Enables the select command without any pre-configured scope." +commands.allow = ["select"] + +[[permission]] +identifier = "deny-select" +description = "Denies the select command without any pre-configured scope." +commands.deny = ["select"] diff --git a/plugins/sql/permissions/autogenerated/reference.md b/plugins/sql/permissions/autogenerated/reference.md new file mode 100644 index 0000000..c829b10 --- /dev/null +++ b/plugins/sql/permissions/autogenerated/reference.md @@ -0,0 +1,131 @@ +## Default Permission + +### Default Permissions + +This permission set configures what kind of +database operations are available from the sql plugin. + +### Granted Permissions + +All reading related operations are enabled. +Also allows to load or close a connection. + +#### This default permission set includes the following: + +- `allow-close` +- `allow-load` +- `allow-select` + +## Permission Table + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
IdentifierDescription
+ +`sql:allow-close` + + + +Enables the close command without any pre-configured scope. + +
+ +`sql:deny-close` + + + +Denies the close command without any pre-configured scope. + +
+ +`sql:allow-execute` + + + +Enables the execute command without any pre-configured scope. + +
+ +`sql:deny-execute` + + + +Denies the execute command without any pre-configured scope. + +
+ +`sql:allow-load` + + + +Enables the load command without any pre-configured scope. + +
+ +`sql:deny-load` + + + +Denies the load command without any pre-configured scope. + +
+ +`sql:allow-select` + + + +Enables the select command without any pre-configured scope. + +
+ +`sql:deny-select` + + + +Denies the select command without any pre-configured scope. + +
diff --git a/plugins/sql/permissions/default.toml b/plugins/sql/permissions/default.toml new file mode 100644 index 0000000..eb5fa55 --- /dev/null +++ b/plugins/sql/permissions/default.toml @@ -0,0 +1,16 @@ +"$schema" = "schemas/schema.json" + +[default] +description = """ +### Default Permissions + +This permission set configures what kind of +database operations are available from the sql plugin. + +### Granted Permissions + +All reading related operations are enabled. +Also allows to load or close a connection. + +""" +permissions = ["allow-close", "allow-load", "allow-select"] diff --git a/plugins/sql/permissions/schemas/schema.json b/plugins/sql/permissions/schemas/schema.json new file mode 100644 index 0000000..488a953 --- /dev/null +++ b/plugins/sql/permissions/schemas/schema.json @@ -0,0 +1,354 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "PermissionFile", + "description": "Permission file that can define a default permission, a set of permissions or a list of inlined permissions.", + "type": "object", + "properties": { + "default": { + "description": "The default permission set for the plugin", + "anyOf": [ + { + "$ref": "#/definitions/DefaultPermission" + }, + { + "type": "null" + } + ] + }, + "set": { + "description": "A list of permissions sets defined", + "type": "array", + "items": { + "$ref": "#/definitions/PermissionSet" + } + }, + "permission": { + "description": "A list of inlined permissions", + "default": [], + "type": "array", + "items": { + "$ref": "#/definitions/Permission" + } + } + }, + "definitions": { + "DefaultPermission": { + "description": "The default permission set of the plugin.\n\nWorks similarly to a permission with the \"default\" identifier.", + "type": "object", + "required": [ + "permissions" + ], + "properties": { + "version": { + "description": "The version of the permission.", + "type": [ + "integer", + "null" + ], + "format": "uint64", + "minimum": 1.0 + }, + "description": { + "description": "Human-readable description of what the permission does. Tauri convention is to use `