(chore): update codebase
This commit is contained in:
parent
fd15215f15
commit
4e8d46b20e
106 changed files with 29936 additions and 1023 deletions
659
apps/tauri/src-tauri/Cargo.lock
generated
659
apps/tauri/src-tauri/Cargo.lock
generated
File diff suppressed because it is too large
Load diff
|
|
@ -15,42 +15,47 @@ name = "mobile_lib"
|
|||
crate-type = ["staticlib", "cdylib", "rlib"]
|
||||
|
||||
[build-dependencies]
|
||||
tauri-build = { version = "2", features = [] }
|
||||
tauri-build = { version = "2.6", features = [] }
|
||||
|
||||
[dependencies]
|
||||
base64 = "0.22"
|
||||
image = { version = "0.25", default-features = false, features = ["jpeg"] }
|
||||
tauri-plugin-opener = "2"
|
||||
tauri-plugin-opener = "2.5.3"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
tauri-plugin-deep-link = "2"
|
||||
tauri-plugin-deep-link = "2.4.7"
|
||||
|
||||
[target.'cfg(any(target_os = "linux", target_os = "windows", target_os = "macos"))'.dependencies]
|
||||
xcap = "0.4.1"
|
||||
|
||||
[target.'cfg(target_os = "android")'.dependencies.tauri]
|
||||
version = "2"
|
||||
version = "2.11"
|
||||
features = []
|
||||
default-features = true
|
||||
|
||||
[target.'cfg(target_os = "windows")'.dependencies.tauri]
|
||||
version = "2"
|
||||
version = "2.11"
|
||||
features = ["compression", "common-controls-v6", "dynamic-acl"]
|
||||
default-features = true
|
||||
|
||||
[target.'cfg(target_os = "linux")'.dependencies.tauri]
|
||||
version = "2"
|
||||
version = "2.11"
|
||||
features = ["common-controls-v6", "cef", "compression", "dynamic-acl", "x11"]
|
||||
default-features = false
|
||||
|
||||
[target.'cfg(target_os = "macos")'.dependencies.tauri]
|
||||
version = "2"
|
||||
version = "2.11"
|
||||
features = ["x11", "common-controls-v6", "cef", "compression", "dynamic-acl"]
|
||||
default-features = false
|
||||
|
||||
[target.'cfg(any(target_os = "android", target_os = "ios"))'.dependencies]
|
||||
tauri-plugin-barcode-scanner = "2"
|
||||
tauri-plugin-barcode-scanner = "2.4.4"
|
||||
|
||||
[patch.crates-io.tauri]
|
||||
git = "https://github.com/tauri-apps/tauri"
|
||||
branch = "feat/cef"
|
||||
|
||||
[patch."https://github.com/tauri-apps/tauri"]
|
||||
tauri-runtime = { path = "vendor/tauri-runtime" }
|
||||
tauri-runtime-cef = { path = "vendor/tauri-runtime-cef" }
|
||||
tauri-utils = { path = "vendor/tauri-utils" }
|
||||
|
|
|
|||
|
|
@ -25,8 +25,6 @@
|
|||
<!-- DEEP LINK PLUGIN. AUTO-GENERATED. DO NOT REMOVE. -->
|
||||
<intent-filter >
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<!-- ChromeOS ARC++ uses a different action for deep links -->
|
||||
<action android:name="org.chromium.arc.intent.action.VIEW" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
<data android:scheme="tensamin" />
|
||||
|
|
|
|||
|
|
@ -1,3 +1,6 @@
|
|||
import com.android.build.api.dsl.ApplicationExtension
|
||||
import com.android.build.api.dsl.LibraryExtension
|
||||
|
||||
buildscript {
|
||||
repositories {
|
||||
google()
|
||||
|
|
@ -5,7 +8,7 @@ buildscript {
|
|||
}
|
||||
dependencies {
|
||||
classpath("com.android.tools.build:gradle:8.11.0")
|
||||
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.9.25")
|
||||
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:2.1.21")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -16,7 +19,24 @@ allprojects {
|
|||
}
|
||||
}
|
||||
|
||||
subprojects {
|
||||
plugins.withId("com.android.application") {
|
||||
extensions.configure<ApplicationExtension>("android") {
|
||||
lint {
|
||||
checkReleaseBuilds = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
plugins.withId("com.android.library") {
|
||||
extensions.configure<LibraryExtension>("android") {
|
||||
lint {
|
||||
checkReleaseBuilds = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tasks.register("clean").configure {
|
||||
delete("build")
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,10 +1,13 @@
|
|||
import java.io.File
|
||||
import javax.inject.Inject
|
||||
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.Internal
|
||||
import org.gradle.api.tasks.TaskAction
|
||||
import org.gradle.process.ExecOperations
|
||||
|
||||
open class BuildTask : DefaultTask() {
|
||||
@Input
|
||||
|
|
@ -14,6 +17,13 @@ open class BuildTask : DefaultTask() {
|
|||
@Input
|
||||
var release: Boolean? = null
|
||||
|
||||
@Internal
|
||||
var baseProjectDir: File? = null
|
||||
|
||||
@get:Inject
|
||||
protected open val execOperations: ExecOperations
|
||||
get() = throw UnsupportedOperationException("Gradle injects ExecOperations")
|
||||
|
||||
@TaskAction
|
||||
fun assemble() {
|
||||
val executable = """bun""";
|
||||
|
|
@ -48,15 +58,16 @@ open class BuildTask : DefaultTask() {
|
|||
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 baseProjectDir = baseProjectDir ?: throw GradleException("baseProjectDir cannot be null")
|
||||
val args = listOf("tauri", "android", "android-studio-script");
|
||||
|
||||
project.exec {
|
||||
workingDir(File(project.projectDir, rootDirRel))
|
||||
execOperations.exec {
|
||||
workingDir(File(baseProjectDir, rootDirRel))
|
||||
executable(executable)
|
||||
args(args)
|
||||
if (project.logger.isEnabled(LogLevel.DEBUG)) {
|
||||
if (logger.isEnabled(LogLevel.DEBUG)) {
|
||||
args("-vv")
|
||||
} else if (project.logger.isEnabled(LogLevel.INFO)) {
|
||||
} else if (logger.isEnabled(LogLevel.INFO)) {
|
||||
args("-v")
|
||||
}
|
||||
if (release) {
|
||||
|
|
@ -65,4 +76,4 @@ open class BuildTask : DefaultTask() {
|
|||
args(listOf("--target", target))
|
||||
}.assertNormalExitValue()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -72,6 +72,7 @@ open class RustPlugin : Plugin<Project> {
|
|||
rootDirRel = config.rootDirRel
|
||||
target = targetName
|
||||
release = profile == "release"
|
||||
baseProjectDir = project.projectDir
|
||||
}
|
||||
|
||||
buildTask.dependsOn(targetBuildTask)
|
||||
|
|
@ -82,4 +83,4 @@ open class RustPlugin : Plugin<Project> {
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -183,8 +183,31 @@ fn get_screen_share_capabilities() -> ScreenShareCapabilities {
|
|||
}
|
||||
}
|
||||
|
||||
fn setup_app<R: tauri::Runtime>(
|
||||
_app: &mut tauri::App<R>,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
#[cfg(any(target_os = "linux", windows))]
|
||||
{
|
||||
use tauri_plugin_deep_link::DeepLinkExt;
|
||||
|
||||
if let Err(error) = _app.deep_link().register_all() {
|
||||
if cfg!(debug_assertions) {
|
||||
eprintln!("Skipping deep link registration during dev: {error}");
|
||||
} else {
|
||||
return Err(Box::new(error));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||
pub fn run() {
|
||||
#[cfg(any(target_os = "linux", target_os = "macos"))]
|
||||
let builder = tauri::Builder::<tauri::Cef>::default();
|
||||
|
||||
#[cfg(not(any(target_os = "linux", target_os = "macos")))]
|
||||
let builder = tauri::Builder::default();
|
||||
|
||||
#[cfg(any(target_os = "linux", target_os = "macos"))]
|
||||
|
|
@ -207,21 +230,7 @@ pub fn run() {
|
|||
let builder = builder.plugin(tauri_plugin_barcode_scanner::init());
|
||||
|
||||
builder
|
||||
.setup(|_app| {
|
||||
#[cfg(any(target_os = "linux", windows))]
|
||||
{
|
||||
use tauri_plugin_deep_link::DeepLinkExt;
|
||||
|
||||
if let Err(error) = _app.deep_link().register_all() {
|
||||
if cfg!(debug_assertions) {
|
||||
eprintln!("Skipping deep link registration during dev: {error}");
|
||||
} else {
|
||||
return Err(error.into());
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
.setup(setup_app)
|
||||
.invoke_handler(tauri::generate_handler![
|
||||
list_screen_share_sources,
|
||||
list_audio_outputs,
|
||||
|
|
|
|||
12
apps/tauri/src-tauri/vendor/Cargo.toml
vendored
Normal file
12
apps/tauri/src-tauri/vendor/Cargo.toml
vendored
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
[workspace]
|
||||
members = ["tauri-runtime", "tauri-runtime-cef", "tauri-utils"]
|
||||
resolver = "2"
|
||||
|
||||
[workspace.package]
|
||||
authors = ["Tauri Programme within The Commons Conservancy"]
|
||||
homepage = "https://tauri.app/"
|
||||
repository = "https://github.com/tauri-apps/tauri"
|
||||
categories = ["gui", "web-programming"]
|
||||
license = "Apache-2.0 OR MIT"
|
||||
edition = "2024"
|
||||
rust-version = "1.88"
|
||||
55
apps/tauri/src-tauri/vendor/tauri-runtime-cef/Cargo.toml
vendored
Normal file
55
apps/tauri/src-tauri/vendor/tauri-runtime-cef/Cargo.toml
vendored
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
[package]
|
||||
name = "tauri-runtime-cef"
|
||||
version = "0.1.0"
|
||||
authors.workspace = true
|
||||
homepage.workspace = true
|
||||
repository.workspace = true
|
||||
categories.workspace = true
|
||||
license.workspace = true
|
||||
edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
|
||||
[dependencies]
|
||||
dioxus-debug-cell = "0.1"
|
||||
tauri-runtime = { version = "2.9.2", path = "../tauri-runtime" }
|
||||
tauri-utils = { version = "2.8.0", path = "../tauri-utils", features = [
|
||||
"html-manipulation",
|
||||
] }
|
||||
html5ever = "0.29"
|
||||
raw-window-handle = "0.6"
|
||||
url = "2"
|
||||
http = "1"
|
||||
cef = { version = "=146.4.1", default-features = false }
|
||||
# Not actually used directly, just locking it.
|
||||
cef-dll-sys = "=146.4.1"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
kuchiki = { package = "kuchikiki", version = "0.8.8-speedreader" }
|
||||
sha2 = "0.10"
|
||||
base64 = "0.22"
|
||||
dirs = "6"
|
||||
|
||||
[target."cfg(windows)".dependencies]
|
||||
windows = { version = "0.61", features = [
|
||||
"Win32_Graphics",
|
||||
"Win32_Graphics_Gdi",
|
||||
"Win32_UI_HiDpi",
|
||||
"Win32_UI_Input",
|
||||
"Win32_UI_Input_KeyboardAndMouse",
|
||||
"Win32_System_LibraryLoader",
|
||||
] }
|
||||
|
||||
[target."cfg(target_os = \"macos\")".dependencies]
|
||||
objc2 = "0.6"
|
||||
objc2-app-kit = { version = "0.3", features = [] }
|
||||
objc2-foundation = { version = "0.3", features = ["NSNotification"] }
|
||||
|
||||
[target."cfg(any(target_os = \"linux\", target_os = \"dragonfly\", target_os = \"freebsd\", target_os = \"openbsd\", target_os = \"netbsd\"))".dependencies]
|
||||
gtk = { version = "0.18", features = ["v3_24"] }
|
||||
x11-dl = "2.21"
|
||||
|
||||
[features]
|
||||
default = ["sandbox"]
|
||||
devtools = []
|
||||
macos-private-api = ["tauri-runtime/macos-private-api"]
|
||||
sandbox = ["cef/sandbox"]
|
||||
4351
apps/tauri/src-tauri/vendor/tauri-runtime-cef/src/cef_impl.rs
vendored
Normal file
4351
apps/tauri/src-tauri/vendor/tauri-runtime-cef/src/cef_impl.rs
vendored
Normal file
File diff suppressed because it is too large
Load diff
82
apps/tauri/src-tauri/vendor/tauri-runtime-cef/src/cef_impl/cookie.rs
vendored
Normal file
82
apps/tauri/src-tauri/vendor/tauri-runtime-cef/src/cef_impl/cookie.rs
vendored
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
use cef::{rc::*, *};
|
||||
use std::sync::{Arc, Mutex, mpsc::Sender};
|
||||
|
||||
cef::wrap_cookie_visitor! {
|
||||
pub struct CollectUrlCookiesVisitor {
|
||||
pub tx: Sender<tauri_runtime::Result<Vec<tauri_runtime::Cookie<'static>>>>,
|
||||
pub collected: Arc<Mutex<Vec<tauri_runtime::Cookie<'static>>>>,
|
||||
}
|
||||
|
||||
impl CookieVisitor {
|
||||
fn visit(
|
||||
&self,
|
||||
cookie: Option<&cef::Cookie>,
|
||||
count: ::std::os::raw::c_int,
|
||||
total: ::std::os::raw::c_int,
|
||||
_delete_cookie: Option<&mut ::std::os::raw::c_int>,
|
||||
) -> ::std::os::raw::c_int {
|
||||
if let Some(c) = cookie {
|
||||
let name = c.name.to_string();
|
||||
let value = c.value.to_string();
|
||||
let domain = c.domain.to_string();
|
||||
let path = c.path.to_string();
|
||||
|
||||
let mut builder = tauri_runtime::Cookie::build((name, value));
|
||||
if !domain.is_empty() { builder = builder.domain(domain); }
|
||||
if !path.is_empty() { builder = builder.path(path); }
|
||||
if c.secure == 1 { builder = builder.secure(true); }
|
||||
if c.httponly == 1 { builder = builder.http_only(true); }
|
||||
let ck = builder.build();
|
||||
|
||||
self.collected.lock().unwrap().push(ck.into_owned());
|
||||
}
|
||||
|
||||
if (count + 1) >= total {
|
||||
let _ = self.tx.send(Ok(self.collected.lock().unwrap().clone()));
|
||||
}
|
||||
1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
cef::wrap_cookie_visitor! {
|
||||
pub struct CollectAllCookiesVisitor {
|
||||
pub tx: Sender<tauri_runtime::Result<Vec<tauri_runtime::Cookie<'static>>>>,
|
||||
pub collected: Arc<Mutex<Vec<tauri_runtime::Cookie<'static>>>>,
|
||||
}
|
||||
|
||||
impl CookieVisitor {
|
||||
fn visit(
|
||||
&self,
|
||||
cookie: Option<&cef::Cookie>,
|
||||
count: ::std::os::raw::c_int,
|
||||
total: ::std::os::raw::c_int,
|
||||
_delete_cookie: Option<&mut ::std::os::raw::c_int>,
|
||||
) -> ::std::os::raw::c_int {
|
||||
if let Some(c) = cookie {
|
||||
let name = c.name.to_string();
|
||||
let value = c.value.to_string();
|
||||
let domain = c.domain.to_string();
|
||||
let path = c.path.to_string();
|
||||
|
||||
let mut builder = tauri_runtime::Cookie::build((name, value));
|
||||
if !domain.is_empty() { builder = builder.domain(domain); }
|
||||
if !path.is_empty() { builder = builder.path(path); }
|
||||
if c.secure == 1 { builder = builder.secure(true); }
|
||||
if c.httponly == 1 { builder = builder.http_only(true); }
|
||||
let ck = builder.build();
|
||||
|
||||
self.collected.lock().unwrap().push(ck.into_owned());
|
||||
}
|
||||
|
||||
if (count + 1) >= total {
|
||||
let _ = self.tx.send(Ok(self.collected.lock().unwrap().clone()));
|
||||
}
|
||||
1
|
||||
}
|
||||
}
|
||||
}
|
||||
72
apps/tauri/src-tauri/vendor/tauri-runtime-cef/src/cef_impl/drag_window.rs
vendored
Normal file
72
apps/tauri/src-tauri/vendor/tauri-runtime-cef/src/cef_impl/drag_window.rs
vendored
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
#[cfg(windows)]
|
||||
pub mod windows {
|
||||
use cef::*;
|
||||
use windows::Win32::Foundation::*;
|
||||
use windows::Win32::UI::WindowsAndMessaging::*;
|
||||
use windows::core::{PCWSTR, w};
|
||||
|
||||
/// Same as [WNDPROC] but without the Option wrapper.
|
||||
type WindowProc = unsafe extern "system" fn(HWND, u32, WPARAM, LPARAM) -> LRESULT;
|
||||
|
||||
const ORIGINAL_WND_PROP: PCWSTR = w!("TAURI_CEF_ORIGINAL_WND_PROC");
|
||||
|
||||
/// Subclasses the given window to handle draggable regions
|
||||
/// by replacing its window procedure with `root_window_proc`
|
||||
/// and storing the original procedure as a property to be called later.
|
||||
pub fn subclass_window_for_dragging(window: &mut cef::Window) {
|
||||
let hwnd = window.window_handle();
|
||||
let hwnd = HWND(hwnd.0 as _);
|
||||
subclass_window(hwnd, root_window_proc);
|
||||
}
|
||||
|
||||
/// Subclasses a window by replacing its window procedure with the given `proc`
|
||||
/// and storing the original procedure as a property for later use.
|
||||
fn subclass_window(hwnd: HWND, proc: WindowProc) {
|
||||
// If already subclassed, return early
|
||||
let orginial_wnd_proc = unsafe { GetPropW(hwnd, ORIGINAL_WND_PROP) };
|
||||
if !orginial_wnd_proc.is_invalid() {
|
||||
return;
|
||||
}
|
||||
|
||||
// Reset last error
|
||||
unsafe { SetLastError(ERROR_SUCCESS) };
|
||||
|
||||
// Set the new window procedure and get the orginal one
|
||||
let original_wnd_proc = unsafe { SetWindowLongPtrW(hwnd, GWLP_WNDPROC, proc as isize) };
|
||||
if original_wnd_proc == 0 && unsafe { GetLastError() } != ERROR_SUCCESS {
|
||||
return;
|
||||
}
|
||||
|
||||
unsafe {
|
||||
// Store the original window proc as a property for later use
|
||||
let _ = SetPropW(
|
||||
hwnd,
|
||||
ORIGINAL_WND_PROP,
|
||||
Some(HANDLE(original_wnd_proc as _)),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// The root window procedure to handle WM_NCLBUTTONDOWN
|
||||
/// by calling DefWindowProcW directly to allow dragging
|
||||
/// and forwarding other messages to the original CEF window procedure.
|
||||
unsafe extern "system" fn root_window_proc(
|
||||
hwnd: HWND,
|
||||
msg: u32,
|
||||
wparam: WPARAM,
|
||||
lparam: LPARAM,
|
||||
) -> LRESULT {
|
||||
if msg == WM_NCLBUTTONDOWN {
|
||||
return unsafe { DefWindowProcW(hwnd, msg, wparam, lparam) };
|
||||
}
|
||||
|
||||
// For other messages, call the original CEF window procedure
|
||||
let original_wnd_proc = GetPropW(hwnd, ORIGINAL_WND_PROP);
|
||||
let original_wnd_proc = std::mem::transmute::<_, WindowProc>(original_wnd_proc.0);
|
||||
CallWindowProcW(Some(original_wnd_proc), hwnd, msg, wparam, lparam)
|
||||
}
|
||||
}
|
||||
432
apps/tauri/src-tauri/vendor/tauri-runtime-cef/src/cef_impl/request_handler.rs
vendored
Normal file
432
apps/tauri/src-tauri/vendor/tauri-runtime-cef/src/cef_impl/request_handler.rs
vendored
Normal file
|
|
@ -0,0 +1,432 @@
|
|||
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
use std::{
|
||||
borrow::Cow,
|
||||
io::{Cursor, Read},
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
use cef::{rc::*, *};
|
||||
use dioxus_debug_cell::RefCell;
|
||||
use html5ever::{LocalName, interface::QualName, namespace_url, ns};
|
||||
use http::{
|
||||
HeaderMap, HeaderName, HeaderValue,
|
||||
header::{CONTENT_SECURITY_POLICY, CONTENT_TYPE},
|
||||
};
|
||||
use kuchiki::NodeRef;
|
||||
use tauri_runtime::webview::UriSchemeProtocolHandler;
|
||||
use tauri_utils::{
|
||||
config::{Csp, CspDirectiveSources},
|
||||
html::{parse as parse_html, serialize_node},
|
||||
};
|
||||
use url::Url;
|
||||
|
||||
use super::CefInitScript;
|
||||
|
||||
type HttpResponse = Arc<RefCell<Option<http::Response<Cursor<Vec<u8>>>>>>;
|
||||
|
||||
fn csp_inject_initialization_scripts_hashes(
|
||||
existing_csp: String,
|
||||
initialization_scripts: &[CefInitScript],
|
||||
) -> String {
|
||||
if initialization_scripts.is_empty() {
|
||||
return existing_csp;
|
||||
}
|
||||
|
||||
// For custom schemes, include ALL script hashes (we inject all scripts into HTML)
|
||||
// This matches the HTML injection behavior in inject_scripts_into_html_body
|
||||
let script_hashes: Vec<String> = initialization_scripts
|
||||
.iter()
|
||||
.map(|s| s.hash.clone())
|
||||
.collect();
|
||||
|
||||
if script_hashes.is_empty() {
|
||||
return existing_csp;
|
||||
}
|
||||
|
||||
// Parse CSP using tauri-utils
|
||||
let mut csp_map: std::collections::HashMap<String, CspDirectiveSources> =
|
||||
Csp::Policy(existing_csp.to_string()).into();
|
||||
|
||||
// Update or create script-src directive with script hashes
|
||||
let script_src = csp_map
|
||||
.entry("script-src".to_string())
|
||||
.or_insert_with(|| CspDirectiveSources::List(vec!["'self'".to_string()]));
|
||||
|
||||
// Extend with script hashes
|
||||
script_src.extend(script_hashes);
|
||||
|
||||
// Convert back to CSP string
|
||||
Csp::DirectiveMap(csp_map).to_string()
|
||||
}
|
||||
|
||||
/// Helper function to inject initialization scripts into HTML body
|
||||
fn inject_scripts_into_html_body(
|
||||
body: &[u8],
|
||||
initialization_scripts: &[CefInitScript],
|
||||
) -> Option<Vec<u8>> {
|
||||
// Check if body is valid UTF-8 HTML
|
||||
let Ok(body_str) = std::str::from_utf8(body) else {
|
||||
return None;
|
||||
};
|
||||
|
||||
// Parse HTML and inject scripts
|
||||
let document = parse_html(body_str.to_string());
|
||||
|
||||
let head = if let Ok(ref head_node) = document.select_first("head") {
|
||||
head_node.as_node().clone()
|
||||
} else {
|
||||
let head_node = NodeRef::new_element(
|
||||
QualName::new(None, ns!(html), LocalName::from("head")),
|
||||
None,
|
||||
);
|
||||
document.prepend(head_node.clone());
|
||||
head_node
|
||||
};
|
||||
|
||||
// Inject initialization scripts (for custom schemes, inject all scripts)
|
||||
for init_script in initialization_scripts.iter().rev() {
|
||||
let script_el = NodeRef::new_element(QualName::new(None, ns!(html), "script".into()), None);
|
||||
script_el.append(NodeRef::new_text(init_script.script.script.as_str()));
|
||||
head.prepend(script_el);
|
||||
}
|
||||
|
||||
// Serialize the modified HTML
|
||||
Some(serialize_node(&document))
|
||||
}
|
||||
|
||||
wrap_resource_request_handler! {
|
||||
pub struct WebResourceRequestHandler {
|
||||
initialization_scripts: Arc<Vec<CefInitScript>>,
|
||||
}
|
||||
|
||||
impl ResourceRequestHandler {
|
||||
|
||||
|
||||
fn on_before_resource_load(
|
||||
&self,
|
||||
_browser: Option<&mut Browser>,
|
||||
_frame: Option<&mut Frame>,
|
||||
_request: Option<&mut Request>,
|
||||
_callback: Option<&mut Callback>,
|
||||
) -> ReturnValue {
|
||||
sys::cef_return_value_t::RV_CONTINUE.into()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
wrap_request_handler! {
|
||||
pub struct WebRequestHandler {
|
||||
initialization_scripts: Arc<Vec<CefInitScript>>,
|
||||
navigation_handler: Option<Arc<tauri_runtime::webview::NavigationHandler>>,
|
||||
}
|
||||
|
||||
impl RequestHandler {
|
||||
fn on_before_browse(
|
||||
&self,
|
||||
_browser: Option<&mut Browser>,
|
||||
frame: Option<&mut Frame>,
|
||||
request: Option<&mut Request>,
|
||||
_user_gesture: ::std::os::raw::c_int,
|
||||
_is_redirect: ::std::os::raw::c_int,
|
||||
) -> ::std::os::raw::c_int {
|
||||
let Some(frame) = frame else {
|
||||
return 0;
|
||||
};
|
||||
// we only fire main frame navigation events to match the behavior of the wry runtime
|
||||
if frame.is_main() == 0 {
|
||||
return 0;
|
||||
}
|
||||
let Some(handler) = &self.navigation_handler else {
|
||||
return 0;
|
||||
};
|
||||
let Some(request) = request else {
|
||||
return 0;
|
||||
};
|
||||
|
||||
let url_str = CefString::from(&request.url()).to_string();
|
||||
let Ok(url) = url::Url::parse(&url_str) else {
|
||||
return 0;
|
||||
};
|
||||
let should_navigate = handler(&url);
|
||||
if should_navigate {
|
||||
0
|
||||
} else {
|
||||
1
|
||||
}
|
||||
}
|
||||
|
||||
fn resource_request_handler(
|
||||
&self,
|
||||
_browser: Option<&mut Browser>,
|
||||
_frame: Option<&mut Frame>,
|
||||
_request: Option<&mut Request>,
|
||||
_is_navigation: ::std::os::raw::c_int,
|
||||
_is_download: ::std::os::raw::c_int,
|
||||
_request_initiator: Option<&CefString>,
|
||||
_disable_default_handling: Option<&mut ::std::os::raw::c_int>,
|
||||
) -> Option<ResourceRequestHandler> {
|
||||
Some(WebResourceRequestHandler::new(
|
||||
self.initialization_scripts.clone(),
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
wrap_resource_handler! {
|
||||
pub struct WebResourceHandler {
|
||||
webview_label: String,
|
||||
handler: Arc<Box<UriSchemeProtocolHandler>>,
|
||||
initialization_scripts: Arc<Vec<CefInitScript>>,
|
||||
// we clone response to send it to the handler thread
|
||||
response: HttpResponse,
|
||||
}
|
||||
|
||||
impl ResourceHandler {
|
||||
fn process_request(
|
||||
&self,
|
||||
request: Option<&mut Request>,
|
||||
callback: Option<&mut Callback>,
|
||||
) -> ::std::os::raw::c_int {
|
||||
let Some(request) = request else { return 0 };
|
||||
let Some(callback) = callback else { return 0 };
|
||||
|
||||
let url = CefString::from(&request.url()).to_string();
|
||||
let url = Url::parse(&url).ok();
|
||||
|
||||
if let Some(url) = url {
|
||||
let callback = ThreadSafe(callback.clone());
|
||||
let response_store = ThreadSafe(self.response.clone());
|
||||
let initialization_scripts = self.initialization_scripts.clone();
|
||||
let responder = Box::new(move |response: http::Response<Cow<'static, [u8]>>| {
|
||||
// Check if this is an HTML response that needs script injection
|
||||
let content_type = response.headers().get(CONTENT_TYPE);
|
||||
let is_html = content_type
|
||||
.and_then(|ct| ct.to_str().ok())
|
||||
.map(|ct| ct.to_lowercase().starts_with("text/html"))
|
||||
.unwrap_or(false);
|
||||
|
||||
let (parts, body) = response.into_parts();
|
||||
let body_bytes = body.into_owned();
|
||||
|
||||
let modified_body = if is_html {
|
||||
inject_scripts_into_html_body(&body_bytes, &initialization_scripts)
|
||||
.unwrap_or(body_bytes)
|
||||
} else {
|
||||
body_bytes
|
||||
};
|
||||
|
||||
let mut response = http::Response::from_parts(parts, Cursor::new(modified_body));
|
||||
|
||||
|
||||
let csp = response
|
||||
.headers_mut()
|
||||
.get_mut(CONTENT_SECURITY_POLICY);
|
||||
|
||||
if let Some(csp) = csp {
|
||||
let csp_string = csp.to_str().unwrap().to_string();
|
||||
let new_csp = csp_inject_initialization_scripts_hashes(
|
||||
csp_string,
|
||||
&initialization_scripts,
|
||||
);
|
||||
*csp = HeaderValue::from_str(&new_csp).unwrap();
|
||||
}
|
||||
|
||||
|
||||
response_store.into_owned().borrow_mut().replace(response);
|
||||
|
||||
let callback = callback.into_owned();
|
||||
callback.cont();
|
||||
});
|
||||
|
||||
let label = self.webview_label.clone();
|
||||
let handler = self.handler.clone();
|
||||
|
||||
let data = read_request_body(request);
|
||||
let headers = get_request_headers(request);
|
||||
let method_str = CefString::from(&request.method()).to_string();
|
||||
let method = http::Method::from_bytes(method_str.as_bytes())
|
||||
.unwrap_or(http::Method::GET);
|
||||
|
||||
std::thread::spawn(move || {
|
||||
let mut http_request = http::Request::builder().method(method).uri(url.as_str()).body(data).unwrap();
|
||||
*http_request.headers_mut() = headers;
|
||||
// handler is Arc<Box<UriSchemeProtocol>>, so we need to dereference to call it
|
||||
(**handler)(&label, http_request, responder);
|
||||
});
|
||||
1
|
||||
} else {
|
||||
0
|
||||
}
|
||||
}
|
||||
|
||||
fn read(
|
||||
&self,
|
||||
data_out: *mut u8,
|
||||
bytes_to_read: ::std::os::raw::c_int,
|
||||
bytes_read: Option<&mut ::std::os::raw::c_int>,
|
||||
_callback: Option<&mut ResourceReadCallback>,
|
||||
) -> ::std::os::raw::c_int {
|
||||
let Ok(bytes_to_read) = usize::try_from(bytes_to_read) else {
|
||||
return 0;
|
||||
};
|
||||
let data_out = unsafe { std::slice::from_raw_parts_mut(data_out, bytes_to_read) };
|
||||
let count = self.response.borrow_mut().as_mut().and_then(|response| response.body_mut().read(data_out).ok()).unwrap_or(0);
|
||||
if let Some(bytes_read) = bytes_read {
|
||||
let Ok(count) = count.try_into() else {
|
||||
return 0;
|
||||
};
|
||||
*bytes_read = count;
|
||||
if count > 0 {
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
0
|
||||
}
|
||||
|
||||
fn response_headers(
|
||||
&self,
|
||||
response: Option<&mut Response>,
|
||||
response_length: Option<&mut i64>,
|
||||
redirect_url: Option<&mut CefString>,
|
||||
) {
|
||||
let (Some(response), Some(response_data)) = (response, &*self.response.borrow()) else { return };
|
||||
|
||||
response.set_status(response_data.status().as_u16() as i32);
|
||||
let mut content_type = None;
|
||||
|
||||
// First pass: collect CSP header and set other headers
|
||||
for (name, value) in response_data.headers() {
|
||||
let Ok(value) = value.to_str() else { continue; };
|
||||
|
||||
response.set_header_by_name(Some(&name.as_str().into()), Some(&value.into()), 0);
|
||||
|
||||
if name == CONTENT_TYPE {
|
||||
content_type.replace(value.to_string());
|
||||
}
|
||||
}
|
||||
|
||||
response.set_header_by_name(
|
||||
Some(&"Cache-Control".into()),
|
||||
Some(&"no-store".into()),
|
||||
1,
|
||||
);
|
||||
|
||||
let mime_type = content_type
|
||||
.as_ref()
|
||||
.and_then(|t| t.split(';').next())
|
||||
.map(str::trim)
|
||||
.unwrap_or("text/plain");
|
||||
response.set_mime_type(Some(&mime_type.into()));
|
||||
|
||||
if let Some(length) = response_length { *length = -1; }
|
||||
|
||||
if let Some(redirect_url) = redirect_url {
|
||||
let _ = std::mem::take(redirect_url);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
wrap_scheme_handler_factory! {
|
||||
pub struct UriSchemeHandlerFactory {
|
||||
registry: super::SchemeHandlerRegistry,
|
||||
scheme: String,
|
||||
}
|
||||
|
||||
impl SchemeHandlerFactory {
|
||||
fn create(
|
||||
&self,
|
||||
browser: Option<&mut Browser>,
|
||||
_frame: Option<&mut Frame>,
|
||||
_scheme_name: Option<&CefString>,
|
||||
_request: Option<&mut Request>,
|
||||
) -> Option<ResourceHandler> {
|
||||
let browser = browser?;
|
||||
let id = browser.identifier();
|
||||
|
||||
// get handler from our regsitry based on browser ID and scheme
|
||||
let (webview_label, handler, initialization_scripts) = self
|
||||
.registry
|
||||
.lock()
|
||||
.unwrap()
|
||||
.get(&(id, self.scheme.clone()))
|
||||
.cloned()?;
|
||||
|
||||
Some(WebResourceHandler::new(webview_label, handler, initialization_scripts, Arc::new(RefCell::new(None))))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct ThreadSafe<T>(T);
|
||||
|
||||
impl<T> ThreadSafe<T> {
|
||||
fn into_owned(self) -> T {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
||||
unsafe impl<T> Send for ThreadSafe<T> {}
|
||||
unsafe impl<T> Sync for ThreadSafe<T> {}
|
||||
|
||||
fn read_request_body(request: &mut Request) -> Vec<u8> {
|
||||
let mut body = Vec::new();
|
||||
|
||||
if let Some(post_data) = request.post_data() {
|
||||
let mut elements = vec![None; post_data.element_count()];
|
||||
post_data.elements(Some(&mut elements));
|
||||
for element in elements.into_iter().flatten() {
|
||||
match element.get_type().as_ref() {
|
||||
sys::cef_postdataelement_type_t::PDE_TYPE_BYTES => {
|
||||
let size = element.bytes_count();
|
||||
if size > 0 {
|
||||
let mut buf = vec![0u8; size];
|
||||
// Copy bytes into our buffer
|
||||
let copied = element.bytes(size, buf.as_mut_ptr());
|
||||
// Safety: CEF promises it wrote `copied` bytes into buf
|
||||
unsafe {
|
||||
buf.set_len(copied);
|
||||
}
|
||||
body.extend(buf);
|
||||
}
|
||||
}
|
||||
sys::cef_postdataelement_type_t::PDE_TYPE_FILE => {
|
||||
// Read file from disk
|
||||
let file_path = CefString::from(&element.file()).to_string();
|
||||
if let Ok(mut file) = std::fs::File::open(&file_path) {
|
||||
use std::io::Read;
|
||||
let mut buf = Vec::new();
|
||||
if file.read_to_end(&mut buf).is_ok() {
|
||||
body.extend(buf);
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
body
|
||||
}
|
||||
|
||||
fn get_request_headers(request: &mut Request) -> HeaderMap {
|
||||
let mut headers = HeaderMap::new();
|
||||
|
||||
let mut map = CefStringMultimap::new();
|
||||
|
||||
request.header_map(Some(&mut map));
|
||||
|
||||
// Iterate through all entries
|
||||
for (name, value) in map {
|
||||
for v in value {
|
||||
headers.append(
|
||||
HeaderName::from_bytes(name.as_bytes()).unwrap(),
|
||||
HeaderValue::from_str(&v).unwrap(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
headers
|
||||
}
|
||||
110
apps/tauri/src-tauri/vendor/tauri-runtime-cef/src/cef_webview.rs
vendored
Normal file
110
apps/tauri/src-tauri/vendor/tauri-runtime-cef/src/cef_webview.rs
vendored
Normal file
|
|
@ -0,0 +1,110 @@
|
|||
use cef::*;
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
mod macos;
|
||||
|
||||
#[cfg(windows)]
|
||||
mod windows;
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
mod linux;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub enum CefWebview {
|
||||
BrowserView(cef::BrowserView),
|
||||
Browser(cef::Browser),
|
||||
}
|
||||
|
||||
impl CefWebview {
|
||||
pub fn is_browser(&self) -> bool {
|
||||
matches!(self, CefWebview::Browser(_))
|
||||
}
|
||||
|
||||
pub fn browser(&self) -> Option<cef::Browser> {
|
||||
match self {
|
||||
CefWebview::BrowserView(view) => view.browser(),
|
||||
CefWebview::Browser(browser) => Some(browser.clone()),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn browser_id(&self) -> i32 {
|
||||
match self {
|
||||
CefWebview::BrowserView(view) => view.browser().map_or(-1, |b| b.identifier()),
|
||||
CefWebview::Browser(browser) => browser.identifier(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_background_color(&self, color: Option<u32>) {
|
||||
if let CefWebview::BrowserView(view) = self {
|
||||
let window = view.window();
|
||||
let color = color.or_else(|| {
|
||||
window.map(|w| w.theme_color(ColorId::COLOR_PRIMARY_BACKGROUND.get_raw() as _))
|
||||
});
|
||||
|
||||
if let Some(color) = color {
|
||||
view.set_background_color(color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn bounds(&self) -> cef::Rect {
|
||||
match self {
|
||||
CefWebview::BrowserView(view) => view.bounds(),
|
||||
CefWebview::Browser(browser) => browser.bounds(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_bounds(&self, rect: Option<&cef::Rect>) {
|
||||
match self {
|
||||
CefWebview::BrowserView(view) => view.set_bounds(rect),
|
||||
CefWebview::Browser(browser) => browser.set_bounds(rect),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn scale_factor(&self) -> f64 {
|
||||
match self {
|
||||
CefWebview::BrowserView(view) => view
|
||||
.window()
|
||||
.and_then(|w| w.display())
|
||||
.map_or(1.0, |d| d.device_scale_factor() as f64),
|
||||
CefWebview::Browser(browser) => browser.scale_factor(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_visible(&self, visible: i32) {
|
||||
match self {
|
||||
CefWebview::BrowserView(view) => view.set_visible(visible),
|
||||
CefWebview::Browser(browser) => browser.set_visible(visible),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn close(&self) {
|
||||
match self {
|
||||
CefWebview::BrowserView(_) => {}
|
||||
CefWebview::Browser(browser) => browser.close(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_parent(&self, parent: &cef::Window) {
|
||||
match self {
|
||||
CefWebview::BrowserView(_) => {}
|
||||
CefWebview::Browser(browser) => browser.set_parent(parent),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
trait CefBrowserExt {
|
||||
fn bounds(&self) -> cef::Rect;
|
||||
fn set_bounds(&self, rect: Option<&cef::Rect>);
|
||||
fn scale_factor(&self) -> f64;
|
||||
fn set_visible(&self, visible: i32);
|
||||
fn close(&self);
|
||||
fn set_parent(&self, parent: &cef::Window);
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
fn nsview(&self) -> Option<objc2::rc::Retained<objc2_app_kit::NSView>>;
|
||||
#[cfg(windows)]
|
||||
fn hwnd(&self) -> Option<::windows::Win32::Foundation::HWND>;
|
||||
#[cfg(target_os = "linux")]
|
||||
fn xid(&self) -> Option<u64>;
|
||||
}
|
||||
208
apps/tauri/src-tauri/vendor/tauri-runtime-cef/src/cef_webview/linux.rs
vendored
Normal file
208
apps/tauri/src-tauri/vendor/tauri-runtime-cef/src/cef_webview/linux.rs
vendored
Normal file
|
|
@ -0,0 +1,208 @@
|
|||
use cef::*;
|
||||
use std::sync::LazyLock;
|
||||
use x11_dl::xlib;
|
||||
|
||||
use crate::cef_webview::CefBrowserExt;
|
||||
|
||||
static X11: LazyLock<Option<xlib::Xlib>> = LazyLock::new(|| xlib::Xlib::open().ok());
|
||||
|
||||
impl CefBrowserExt for cef::Browser {
|
||||
fn xid(&self) -> Option<u64> {
|
||||
let host = self.host()?;
|
||||
let xid = host.window_handle();
|
||||
Some(xid)
|
||||
}
|
||||
|
||||
fn bounds(&self) -> cef::Rect {
|
||||
let Some(xid) = self.xid() else {
|
||||
return cef::Rect::default();
|
||||
};
|
||||
|
||||
let Some(xlib) = X11.as_ref() else {
|
||||
return cef::Rect::default();
|
||||
};
|
||||
|
||||
unsafe {
|
||||
let display = (xlib.XOpenDisplay)(std::ptr::null());
|
||||
if display.is_null() {
|
||||
return cef::Rect::default();
|
||||
}
|
||||
|
||||
let mut root: xlib::Window = 0;
|
||||
let mut x: i32 = 0;
|
||||
let mut y: i32 = 0;
|
||||
let mut width: u32 = 0;
|
||||
let mut height: u32 = 0;
|
||||
let mut border_width: u32 = 0;
|
||||
let mut depth: u32 = 0;
|
||||
|
||||
let status = (xlib.XGetGeometry)(
|
||||
display,
|
||||
xid as xlib::Window,
|
||||
&mut root,
|
||||
&mut x,
|
||||
&mut y,
|
||||
&mut width,
|
||||
&mut height,
|
||||
&mut border_width,
|
||||
&mut depth,
|
||||
);
|
||||
|
||||
(xlib.XCloseDisplay)(display);
|
||||
|
||||
if status == 0 {
|
||||
return cef::Rect::default();
|
||||
}
|
||||
|
||||
// XGetGeometry returns position relative to parent, which is what we need
|
||||
cef::Rect {
|
||||
x,
|
||||
y,
|
||||
width: width as i32,
|
||||
height: height as i32,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn set_bounds(&self, rect: Option<&cef::Rect>) {
|
||||
let Some(rect) = rect else {
|
||||
return;
|
||||
};
|
||||
|
||||
let Some(xid) = self.xid() else {
|
||||
return;
|
||||
};
|
||||
|
||||
let Some(xlib) = X11.as_ref() else {
|
||||
return;
|
||||
};
|
||||
|
||||
unsafe {
|
||||
let display = (xlib.XOpenDisplay)(std::ptr::null());
|
||||
if display.is_null() {
|
||||
return;
|
||||
}
|
||||
|
||||
(xlib.XMoveResizeWindow)(
|
||||
display,
|
||||
xid as xlib::Window,
|
||||
rect.x,
|
||||
rect.y,
|
||||
rect.width as u32,
|
||||
rect.height as u32,
|
||||
);
|
||||
// Ensure window is mapped and raised after setting bounds
|
||||
(xlib.XMapRaised)(display, xid as xlib::Window);
|
||||
(xlib.XFlush)(display);
|
||||
(xlib.XCloseDisplay)(display);
|
||||
}
|
||||
}
|
||||
|
||||
fn scale_factor(&self) -> f64 {
|
||||
// Get scale factor from primary display
|
||||
// CEF on Linux doesn't provide direct access to the window's display,
|
||||
// so we use the primary display as a reasonable default
|
||||
cef::display_get_primary()
|
||||
.map(|d| d.device_scale_factor() as f64)
|
||||
.unwrap_or(1.0)
|
||||
}
|
||||
|
||||
fn set_visible(&self, visible: i32) {
|
||||
let Some(xid) = self.xid() else {
|
||||
return;
|
||||
};
|
||||
|
||||
let Some(xlib) = X11.as_ref() else {
|
||||
return;
|
||||
};
|
||||
|
||||
unsafe {
|
||||
let display = (xlib.XOpenDisplay)(std::ptr::null());
|
||||
if display.is_null() {
|
||||
return;
|
||||
}
|
||||
|
||||
if visible != 0 {
|
||||
(xlib.XMapWindow)(display, xid as xlib::Window);
|
||||
} else {
|
||||
(xlib.XUnmapWindow)(display, xid as xlib::Window);
|
||||
}
|
||||
(xlib.XFlush)(display);
|
||||
(xlib.XCloseDisplay)(display);
|
||||
}
|
||||
}
|
||||
|
||||
fn close(&self) {
|
||||
let Some(xid) = self.xid() else {
|
||||
return;
|
||||
};
|
||||
|
||||
let Some(xlib) = X11.as_ref() else {
|
||||
return;
|
||||
};
|
||||
|
||||
unsafe {
|
||||
let display = (xlib.XOpenDisplay)(std::ptr::null());
|
||||
if display.is_null() {
|
||||
return;
|
||||
}
|
||||
|
||||
(xlib.XDestroyWindow)(display, xid as xlib::Window);
|
||||
(xlib.XFlush)(display);
|
||||
(xlib.XCloseDisplay)(display);
|
||||
}
|
||||
}
|
||||
|
||||
fn set_parent(&self, parent: &cef::Window) {
|
||||
let Some(xid) = self.xid() else {
|
||||
return;
|
||||
};
|
||||
|
||||
let parent_xid = parent.window_handle();
|
||||
if parent_xid == 0 {
|
||||
return;
|
||||
}
|
||||
|
||||
let Some(xlib) = X11.as_ref() else {
|
||||
return;
|
||||
};
|
||||
|
||||
unsafe {
|
||||
let display = (xlib.XOpenDisplay)(std::ptr::null());
|
||||
if display.is_null() {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if window exists before reparenting
|
||||
let mut root: xlib::Window = 0;
|
||||
let mut parent_window: xlib::Window = 0;
|
||||
let mut children: *mut xlib::Window = std::ptr::null_mut();
|
||||
let mut nchildren: u32 = 0;
|
||||
let status = (xlib.XQueryTree)(
|
||||
display,
|
||||
xid as xlib::Window,
|
||||
&mut root,
|
||||
&mut parent_window,
|
||||
&mut children,
|
||||
&mut nchildren,
|
||||
);
|
||||
|
||||
if status != 0 && !children.is_null() {
|
||||
(xlib.XFree)(children as *mut std::ffi::c_void);
|
||||
}
|
||||
|
||||
(xlib.XReparentWindow)(
|
||||
display,
|
||||
xid as xlib::Window,
|
||||
parent_xid as xlib::Window,
|
||||
0,
|
||||
0,
|
||||
);
|
||||
|
||||
// Ensure window is mapped and raised after reparenting
|
||||
(xlib.XMapRaised)(display, xid as xlib::Window);
|
||||
(xlib.XFlush)(display);
|
||||
(xlib.XCloseDisplay)(display);
|
||||
}
|
||||
}
|
||||
}
|
||||
99
apps/tauri/src-tauri/vendor/tauri-runtime-cef/src/cef_webview/macos.rs
vendored
Normal file
99
apps/tauri/src-tauri/vendor/tauri-runtime-cef/src/cef_webview/macos.rs
vendored
Normal file
|
|
@ -0,0 +1,99 @@
|
|||
use crate::cef_webview::CefBrowserExt;
|
||||
use cef::*;
|
||||
use objc2::rc::Retained;
|
||||
use objc2_app_kit::NSView;
|
||||
use objc2_foundation::{NSPoint, NSRect, NSSize};
|
||||
|
||||
impl CefBrowserExt for cef::Browser {
|
||||
fn nsview(&self) -> Option<objc2::rc::Retained<objc2_app_kit::NSView>> {
|
||||
let host = self.host()?;
|
||||
let nsview = host.window_handle() as *mut NSView;
|
||||
unsafe { Retained::<NSView>::retain(nsview) }
|
||||
}
|
||||
|
||||
fn bounds(&self) -> cef::Rect {
|
||||
let Some(nsview) = self.nsview() else {
|
||||
return cef::Rect::default();
|
||||
};
|
||||
|
||||
let parent = unsafe { nsview.superview().unwrap() };
|
||||
let parent_frame = parent.frame();
|
||||
let webview_frame = nsview.frame();
|
||||
|
||||
cef::Rect {
|
||||
x: webview_frame.origin.x as i32,
|
||||
y: (parent_frame.size.height - webview_frame.origin.y - webview_frame.size.height) as i32,
|
||||
width: webview_frame.size.width as i32,
|
||||
height: webview_frame.size.height as i32,
|
||||
}
|
||||
}
|
||||
|
||||
fn set_bounds(&self, rect: Option<&cef::Rect>) {
|
||||
let Some(rect) = rect else {
|
||||
return;
|
||||
};
|
||||
|
||||
let Some(nsview) = self.nsview() else {
|
||||
return;
|
||||
};
|
||||
|
||||
let parent = unsafe { nsview.superview().unwrap() };
|
||||
let parent_frame = parent.frame();
|
||||
|
||||
let origin = NSPoint {
|
||||
x: rect.x as f64,
|
||||
y: (parent_frame.size.height - (rect.y as f64 + rect.height as f64)),
|
||||
};
|
||||
|
||||
let size = NSSize {
|
||||
width: rect.width as f64,
|
||||
height: rect.height as f64,
|
||||
};
|
||||
|
||||
unsafe { nsview.setFrame(NSRect { origin, size }) };
|
||||
}
|
||||
|
||||
fn scale_factor(&self) -> f64 {
|
||||
let Some(nsview) = self.nsview() else {
|
||||
return 1.0;
|
||||
};
|
||||
|
||||
let screen = nsview.window().and_then(|w| w.screen());
|
||||
screen.map(|s| s.backingScaleFactor()).unwrap_or(1.0)
|
||||
}
|
||||
|
||||
fn set_visible(&self, visible: i32) {
|
||||
let Some(nsview) = self.nsview() else {
|
||||
return;
|
||||
};
|
||||
|
||||
if visible != 0 {
|
||||
nsview.setHidden(false);
|
||||
} else {
|
||||
nsview.setHidden(true);
|
||||
}
|
||||
}
|
||||
|
||||
fn close(&self) {
|
||||
let Some(nsview) = self.nsview() else {
|
||||
return;
|
||||
};
|
||||
|
||||
unsafe { nsview.removeFromSuperview() };
|
||||
}
|
||||
|
||||
fn set_parent(&self, parent: &cef::Window) {
|
||||
crate::cef_impl::ensure_valid_content_view(parent.window_handle());
|
||||
|
||||
let Some(nsview) = self.nsview() else {
|
||||
return;
|
||||
};
|
||||
|
||||
let parent_nsview = parent.window_handle();
|
||||
let Some(parent_nsview) = (unsafe { Retained::<NSView>::retain(parent_nsview as _) }) else {
|
||||
return;
|
||||
};
|
||||
|
||||
unsafe { parent_nsview.addSubview(&nsview) };
|
||||
}
|
||||
}
|
||||
210
apps/tauri/src-tauri/vendor/tauri-runtime-cef/src/cef_webview/windows.rs
vendored
Normal file
210
apps/tauri/src-tauri/vendor/tauri-runtime-cef/src/cef_webview/windows.rs
vendored
Normal file
|
|
@ -0,0 +1,210 @@
|
|||
use cef::*;
|
||||
use std::sync::LazyLock;
|
||||
|
||||
use crate::cef_webview::CefBrowserExt;
|
||||
use windows::{
|
||||
Win32::{
|
||||
Foundation::*,
|
||||
Graphics::Gdi::*,
|
||||
System::LibraryLoader::*,
|
||||
UI::{HiDpi::*, WindowsAndMessaging::*},
|
||||
},
|
||||
core::{HRESULT, HSTRING, PCSTR},
|
||||
};
|
||||
|
||||
impl CefBrowserExt for cef::Browser {
|
||||
fn hwnd(&self) -> Option<HWND> {
|
||||
let host = self.host()?;
|
||||
let hwnd = host.window_handle();
|
||||
Some(HWND(hwnd.0 as _))
|
||||
}
|
||||
|
||||
fn bounds(&self) -> cef::Rect {
|
||||
let Some(hwnd) = self.hwnd() else {
|
||||
return cef::Rect::default();
|
||||
};
|
||||
|
||||
let mut rect = RECT::default();
|
||||
let _ = unsafe { GetClientRect(hwnd, &mut rect) };
|
||||
|
||||
let position_point = &mut [POINT {
|
||||
x: rect.left,
|
||||
y: rect.top,
|
||||
}];
|
||||
unsafe { MapWindowPoints(Some(hwnd), GetParent(hwnd).ok(), position_point) };
|
||||
|
||||
cef::Rect {
|
||||
x: position_point[0].x,
|
||||
y: position_point[0].y,
|
||||
width: (rect.right - rect.left) as i32,
|
||||
height: (rect.bottom - rect.top) as i32,
|
||||
}
|
||||
}
|
||||
|
||||
fn set_bounds(&self, rect: Option<&cef::Rect>) {
|
||||
let Some(rect) = rect else {
|
||||
return;
|
||||
};
|
||||
|
||||
let Some(hwnd) = self.hwnd() else {
|
||||
return;
|
||||
};
|
||||
|
||||
let _ = unsafe {
|
||||
SetWindowPos(
|
||||
hwnd,
|
||||
None,
|
||||
rect.x,
|
||||
rect.y,
|
||||
rect.width,
|
||||
rect.height,
|
||||
SWP_ASYNCWINDOWPOS | SWP_NOACTIVATE | SWP_NOZORDER,
|
||||
)
|
||||
};
|
||||
}
|
||||
|
||||
fn scale_factor(&self) -> f64 {
|
||||
let Some(hwnd) = self.hwnd() else {
|
||||
return 1.0;
|
||||
};
|
||||
|
||||
let dpi = unsafe { hwnd_dpi(hwnd) };
|
||||
dpi_to_scale_factor(dpi)
|
||||
}
|
||||
|
||||
fn set_visible(&self, visible: i32) {
|
||||
let Some(hwnd) = self.hwnd() else {
|
||||
return;
|
||||
};
|
||||
|
||||
if visible != 0 {
|
||||
let _ = unsafe { ShowWindow(hwnd, SW_SHOW) };
|
||||
unsafe { ensure_render_target(hwnd) };
|
||||
} else {
|
||||
let _ = unsafe { ShowWindow(hwnd, SW_HIDE) };
|
||||
}
|
||||
}
|
||||
|
||||
fn close(&self) {
|
||||
let Some(hwnd) = self.hwnd() else {
|
||||
return;
|
||||
};
|
||||
|
||||
let _ = unsafe { DestroyWindow(hwnd) };
|
||||
}
|
||||
|
||||
fn set_parent(&self, parent: &cef::Window) {
|
||||
let Some(hwnd) = self.hwnd() else {
|
||||
return;
|
||||
};
|
||||
|
||||
let parent_hwnd = HWND(parent.window_handle().0 as _);
|
||||
let _ = unsafe { SetParent(hwnd, Some(parent_hwnd)) };
|
||||
}
|
||||
}
|
||||
|
||||
/// Toggle visibility on Chrome_WidgetWin_1 children to force CEF to rebind
|
||||
/// the compositor surface and emit a fresh paint after the child webview has
|
||||
/// been hidden.
|
||||
///
|
||||
/// Required when the child has been hidden, particularly when paired with CDP
|
||||
/// `Page.setWebLifecycleState: frozen`, which pauses the renderer's compositor.
|
||||
/// The Chrome_RenderWidgetHostHWND host window survives the freeze, but its
|
||||
/// compositor surface is stale and won't repaint on its own when the parent is
|
||||
/// shown again — leaving the widget visible with blank content. The hide+show
|
||||
/// dance on Chrome_WidgetWin_1 forces Chromium to rebind the surface and
|
||||
/// schedule a paint.
|
||||
unsafe fn ensure_render_target(hwnd: HWND) {
|
||||
use windows::core::PCWSTR;
|
||||
|
||||
const CHROME_WIDGET: PCWSTR = windows::core::w!("Chrome_WidgetWin_1");
|
||||
|
||||
let mut child = unsafe { FindWindowExW(Some(hwnd), None, CHROME_WIDGET, PCWSTR::null()) };
|
||||
while let Ok(child_hwnd) = child {
|
||||
let _ = unsafe { ShowWindow(child_hwnd, SW_HIDE) };
|
||||
let _ = unsafe { ShowWindow(child_hwnd, SW_SHOW) };
|
||||
child = unsafe { FindWindowExW(Some(hwnd), Some(child_hwnd), CHROME_WIDGET, PCWSTR::null()) };
|
||||
}
|
||||
}
|
||||
|
||||
fn get_function_impl(library: &str, function: &str) -> FARPROC {
|
||||
let library = HSTRING::from(library);
|
||||
assert_eq!(function.chars().last(), Some('\0'));
|
||||
|
||||
// Library names we will use are ASCII so we can use the A version to avoid string conversion.
|
||||
let module = unsafe { LoadLibraryW(&library) }.unwrap_or_default();
|
||||
if module.is_invalid() {
|
||||
return None;
|
||||
}
|
||||
|
||||
unsafe { GetProcAddress(module, PCSTR::from_raw(function.as_ptr())) }
|
||||
}
|
||||
|
||||
macro_rules! get_function {
|
||||
($lib:expr, $func:ident) => {
|
||||
get_function_impl($lib, concat!(stringify!($func), '\0'))
|
||||
.map(|f| unsafe { std::mem::transmute::<_, $func>(f) })
|
||||
};
|
||||
}
|
||||
|
||||
pub type GetDpiForWindow = unsafe extern "system" fn(hwnd: HWND) -> u32;
|
||||
pub type GetDpiForMonitor = unsafe extern "system" fn(
|
||||
hmonitor: HMONITOR,
|
||||
dpi_type: MONITOR_DPI_TYPE,
|
||||
dpi_x: *mut u32,
|
||||
dpi_y: *mut u32,
|
||||
) -> HRESULT;
|
||||
|
||||
static GET_DPI_FOR_WINDOW: LazyLock<Option<GetDpiForWindow>> =
|
||||
LazyLock::new(|| get_function!("user32.dll", GetDpiForWindow));
|
||||
static GET_DPI_FOR_MONITOR: LazyLock<Option<GetDpiForMonitor>> =
|
||||
LazyLock::new(|| get_function!("shcore.dll", GetDpiForMonitor));
|
||||
|
||||
pub const BASE_DPI: u32 = 96;
|
||||
pub fn dpi_to_scale_factor(dpi: u32) -> f64 {
|
||||
dpi as f64 / BASE_DPI as f64
|
||||
}
|
||||
|
||||
#[allow(non_snake_case)]
|
||||
pub unsafe fn hwnd_dpi(hwnd: HWND) -> u32 {
|
||||
if let Some(GetDpiForWindow) = *GET_DPI_FOR_WINDOW {
|
||||
// We are on Windows 10 Anniversary Update (1607) or later.
|
||||
match GetDpiForWindow(hwnd) {
|
||||
0 => BASE_DPI, // 0 is returned if hwnd is invalid
|
||||
#[allow(clippy::unnecessary_cast)]
|
||||
dpi => dpi as u32,
|
||||
}
|
||||
} else if let Some(GetDpiForMonitor) = *GET_DPI_FOR_MONITOR {
|
||||
// We are on Windows 8.1 or later.
|
||||
let monitor = MonitorFromWindow(hwnd, MONITOR_DEFAULTTONEAREST);
|
||||
if monitor.is_invalid() {
|
||||
return BASE_DPI;
|
||||
}
|
||||
|
||||
let mut dpi_x = 0;
|
||||
let mut dpi_y = 0;
|
||||
#[allow(clippy::unnecessary_cast)]
|
||||
if GetDpiForMonitor(monitor, MDT_EFFECTIVE_DPI, &mut dpi_x, &mut dpi_y) == S_OK {
|
||||
dpi_x as u32
|
||||
} else {
|
||||
BASE_DPI
|
||||
}
|
||||
} else {
|
||||
let hdc = GetDC(Some(hwnd));
|
||||
if hdc.is_invalid() {
|
||||
return BASE_DPI;
|
||||
}
|
||||
|
||||
// We are on Vista or later.
|
||||
if IsProcessDPIAware().as_bool() {
|
||||
// If the process is DPI aware, then scaling must be handled by the application using
|
||||
// this DPI value.
|
||||
GetDeviceCaps(Some(hdc), LOGPIXELSX) as u32
|
||||
} else {
|
||||
// If the process is DPI unaware, then scaling is performed by the OS; we thus return
|
||||
// 96 (scale factor 1.0) to prevent the window from being re-scaled by both the
|
||||
// application and the WM.
|
||||
BASE_DPI
|
||||
}
|
||||
}
|
||||
}
|
||||
2541
apps/tauri/src-tauri/vendor/tauri-runtime-cef/src/lib.rs
vendored
Normal file
2541
apps/tauri/src-tauri/vendor/tauri-runtime-cef/src/lib.rs
vendored
Normal file
File diff suppressed because it is too large
Load diff
42
apps/tauri/src-tauri/vendor/tauri-runtime-cef/src/utils.rs
vendored
Normal file
42
apps/tauri/src-tauri/vendor/tauri-runtime-cef/src/utils.rs
vendored
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
#[cfg(windows)]
|
||||
pub mod windows {
|
||||
use tauri_runtime::dpi::PhysicalSize;
|
||||
use windows::Win32::Foundation::*;
|
||||
use windows::Win32::UI::WindowsAndMessaging::*;
|
||||
|
||||
pub fn inner_size(hwnd: cef::sys::HWND) -> PhysicalSize<u32> {
|
||||
let hwnd = HWND(hwnd.0 as _);
|
||||
let mut rect = RECT::default();
|
||||
let _ = unsafe { GetClientRect(hwnd, &mut rect) };
|
||||
|
||||
PhysicalSize::new(
|
||||
(rect.right - rect.left) as u32,
|
||||
(rect.bottom - rect.top) as u32,
|
||||
)
|
||||
}
|
||||
|
||||
/// Adjusts the given size to account for window borders, so that the resulting inner size matches the requested size.
|
||||
///
|
||||
/// Expects and returns a size in physical pixels.
|
||||
pub fn adjust_size(hwnd: cef::sys::HWND, size: cef::Size) -> cef::Size {
|
||||
let hwnd = HWND(hwnd.0 as _);
|
||||
|
||||
let mut client_rect = RECT::default();
|
||||
let _ = unsafe { GetClientRect(hwnd, &mut client_rect) };
|
||||
let client_width = client_rect.right - client_rect.left;
|
||||
let client_height = client_rect.bottom - client_rect.top;
|
||||
|
||||
let mut window_rect = RECT::default();
|
||||
let _ = unsafe { GetWindowRect(hwnd, &mut window_rect) };
|
||||
let window_width = window_rect.right - window_rect.left;
|
||||
let window_height = window_rect.bottom - window_rect.top;
|
||||
|
||||
let width_diff = window_width - client_width;
|
||||
let height_diff = window_height - client_height;
|
||||
|
||||
cef::Size {
|
||||
width: size.width + width_diff,
|
||||
height: size.height + height_diff,
|
||||
}
|
||||
}
|
||||
}
|
||||
1083
apps/tauri/src-tauri/vendor/tauri-runtime/CHANGELOG.md
vendored
Normal file
1083
apps/tauri/src-tauri/vendor/tauri-runtime/CHANGELOG.md
vendored
Normal file
File diff suppressed because it is too large
Load diff
62
apps/tauri/src-tauri/vendor/tauri-runtime/Cargo.toml
vendored
Normal file
62
apps/tauri/src-tauri/vendor/tauri-runtime/Cargo.toml
vendored
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
[package]
|
||||
name = "tauri-runtime"
|
||||
version = "2.11.0"
|
||||
description = "Runtime for Tauri applications"
|
||||
exclude = ["CHANGELOG.md", "/target"]
|
||||
readme = "README.md"
|
||||
authors.workspace = true
|
||||
homepage.workspace = true
|
||||
repository.workspace = true
|
||||
categories.workspace = true
|
||||
license.workspace = true
|
||||
edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
all-features = true
|
||||
default-target = "x86_64-unknown-linux-gnu"
|
||||
targets = [
|
||||
"x86_64-pc-windows-msvc",
|
||||
"x86_64-unknown-linux-gnu",
|
||||
"x86_64-apple-darwin",
|
||||
"x86_64-linux-android",
|
||||
"x86_64-apple-ios",
|
||||
]
|
||||
|
||||
[dependencies]
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
thiserror = "2"
|
||||
tauri-utils = { version = "2.9.0", path = "../tauri-utils" }
|
||||
http = "1"
|
||||
raw-window-handle = "0.6"
|
||||
url = { version = "2" }
|
||||
dpi = { version = "0.1", features = ["serde"] }
|
||||
# WARNING: cookie::Cookie is re-exported so bumping this is a breaking change, documented to be done as a minor bump
|
||||
cookie = "0.18"
|
||||
|
||||
[target."cfg(windows)".dependencies.windows]
|
||||
version = "0.61"
|
||||
features = ["Win32_Foundation", "Win32_System_WinRT"]
|
||||
|
||||
[target."cfg(any(target_os = \"linux\", target_os = \"dragonfly\", target_os = \"freebsd\", target_os = \"openbsd\", target_os = \"netbsd\"))".dependencies]
|
||||
gtk = { version = "0.18", features = ["v3_24"] }
|
||||
|
||||
[target."cfg(target_os = \"android\")".dependencies]
|
||||
jni = "0.21"
|
||||
|
||||
[target.'cfg(all(target_vendor = "apple", not(target_os = "macos")))'.dependencies]
|
||||
objc2 = "0.6"
|
||||
objc2-ui-kit = { version = "0.3.0", default-features = false, features = [
|
||||
"UIView",
|
||||
"UIResponder",
|
||||
"UIScene",
|
||||
"UISceneOptions",
|
||||
] }
|
||||
|
||||
[target."cfg(target_os = \"macos\")".dependencies]
|
||||
url = "2"
|
||||
objc2 = "0.6"
|
||||
[features]
|
||||
devtools = []
|
||||
macos-private-api = []
|
||||
177
apps/tauri/src-tauri/vendor/tauri-runtime/LICENSE_APACHE-2.0
vendored
Normal file
177
apps/tauri/src-tauri/vendor/tauri-runtime/LICENSE_APACHE-2.0
vendored
Normal file
|
|
@ -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
|
||||
21
apps/tauri/src-tauri/vendor/tauri-runtime/LICENSE_MIT
vendored
Normal file
21
apps/tauri/src-tauri/vendor/tauri-runtime/LICENSE_MIT
vendored
Normal file
|
|
@ -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.
|
||||
44
apps/tauri/src-tauri/vendor/tauri-runtime/README.md
vendored
Normal file
44
apps/tauri/src-tauri/vendor/tauri-runtime/README.md
vendored
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
# tauri-runtime
|
||||
|
||||
<img align="right" src="https://github.com/tauri-apps/tauri/raw/dev/.github/icon.png" height="128" width="128">
|
||||
|
||||
[](https://github.com/tauri-apps/tauri)
|
||||
[](https://discord.gg/SpmNs4S)
|
||||
|
||||
[](https://github.com/tauri-apps/tauri/actions/workflows/test-core.yml)
|
||||
[](https://tauri.app)
|
||||
|
||||
[](https://good-labs.github.io/greater-good-affirmation)
|
||||
[](https://opencollective.com/tauri)
|
||||
|
||||
| Component | Version |
|
||||
| ------------- | -------------------------------------------------------------------------------------------------------------- |
|
||||
| tauri-runtime | [](https://crates.io/crates/tauri-runtime) |
|
||||
|
||||
## About Tauri
|
||||
|
||||
Tauri is a polyglot and generic system that is very composable and allows engineers to make a wide variety of applications. It is used for building applications for Desktop Computers using a combination of Rust tools and HTML rendered in a Webview. Apps built with Tauri can ship with any number of pieces of an optional JS API / Rust API so that webviews can control the system via message passing. In fact, developers can extend the default API with their own functionality and bridge the Webview and Rust-based backend easily.
|
||||
|
||||
Tauri apps can have custom menus and have tray-type interfaces. They can be updated, and are managed by the user's operating system as expected. They are very small, because they use the system's webview. They do not ship a runtime, since the final binary is compiled from rust. This makes the reversing of Tauri apps not a trivial task.
|
||||
|
||||
## This module
|
||||
|
||||
This is the glue layer between tauri itself and lower level webview libraries.
|
||||
None of the exposed API of this crate is stable, and it may break semver
|
||||
compatibility in the future. The major version only signifies the intended Tauri version.
|
||||
|
||||
To learn more about the details of how all of these pieces fit together, please consult this [ARCHITECTURE.md](https://github.com/tauri-apps/tauri/blob/dev/ARCHITECTURE.md) document.
|
||||
|
||||
## Semver
|
||||
|
||||
**tauri** is following [Semantic Versioning 2.0](https://semver.org/).
|
||||
|
||||
## Licenses
|
||||
|
||||
Code: (c) 2021 - The Tauri Programme within The Commons Conservancy.
|
||||
|
||||
MIT or MIT/Apache 2.0 where applicable.
|
||||
|
||||
Logo: CC-BY-NC-ND
|
||||
|
||||
- Original Tauri Logo Designs by [Daniel Thompson-Yvetot](https://github.com/nothingismagick) and [Guillaume Chau](https://github.com/akryum)
|
||||
19
apps/tauri/src-tauri/vendor/tauri-runtime/build.rs
vendored
Normal file
19
apps/tauri/src-tauri/vendor/tauri-runtime/build.rs
vendored
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
// 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}");
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
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);
|
||||
}
|
||||
60
apps/tauri/src-tauri/vendor/tauri-runtime/src/dpi.rs
vendored
Normal file
60
apps/tauri/src-tauri/vendor/tauri-runtime/src/dpi.rs
vendored
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
pub use dpi::*;
|
||||
use serde::Serialize;
|
||||
|
||||
/// A rectangular region.
|
||||
#[derive(Clone, Copy, Debug, Serialize)]
|
||||
pub struct Rect {
|
||||
/// Rect position.
|
||||
pub position: dpi::Position,
|
||||
/// Rect size.
|
||||
pub size: dpi::Size,
|
||||
}
|
||||
|
||||
impl Default for Rect {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
position: Position::Logical((0, 0).into()),
|
||||
size: Size::Logical((0, 0).into()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A rectangular region in physical pixels.
|
||||
#[derive(Clone, Copy, Debug, Serialize)]
|
||||
pub struct PhysicalRect<P: dpi::Pixel, S: dpi::Pixel> {
|
||||
/// Rect position.
|
||||
pub position: dpi::PhysicalPosition<P>,
|
||||
/// Rect size.
|
||||
pub size: dpi::PhysicalSize<S>,
|
||||
}
|
||||
|
||||
impl<P: dpi::Pixel, S: dpi::Pixel> Default for PhysicalRect<P, S> {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
position: (0, 0).into(),
|
||||
size: (0, 0).into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A rectangular region in logical pixels.
|
||||
#[derive(Clone, Copy, Debug, Serialize)]
|
||||
pub struct LogicalRect<P: dpi::Pixel, S: dpi::Pixel> {
|
||||
/// Rect position.
|
||||
pub position: dpi::LogicalPosition<P>,
|
||||
/// Rect size.
|
||||
pub size: dpi::LogicalSize<S>,
|
||||
}
|
||||
|
||||
impl<P: dpi::Pixel, S: dpi::Pixel> Default for LogicalRect<P, S> {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
position: (0, 0).into(),
|
||||
size: (0, 0).into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
1048
apps/tauri/src-tauri/vendor/tauri-runtime/src/lib.rs
vendored
Normal file
1048
apps/tauri/src-tauri/vendor/tauri-runtime/src/lib.rs
vendored
Normal file
File diff suppressed because it is too large
Load diff
21
apps/tauri/src-tauri/vendor/tauri-runtime/src/monitor.rs
vendored
Normal file
21
apps/tauri/src-tauri/vendor/tauri-runtime/src/monitor.rs
vendored
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
use crate::dpi::{PhysicalPosition, PhysicalRect, PhysicalSize};
|
||||
|
||||
/// Monitor descriptor.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Monitor {
|
||||
/// A human-readable name of the monitor.
|
||||
/// `None` if the monitor doesn't exist anymore.
|
||||
pub name: Option<String>,
|
||||
/// The monitor's resolution.
|
||||
pub size: PhysicalSize<u32>,
|
||||
/// The top-left corner position of the monitor relative to the larger full screen area.
|
||||
pub position: PhysicalPosition<i32>,
|
||||
/// The monitor's work_area.
|
||||
pub work_area: PhysicalRect<i32, u32>,
|
||||
/// Returns the scale factor that can be used to map logical pixels to physical pixels, and vice versa.
|
||||
pub scale_factor: f64,
|
||||
}
|
||||
820
apps/tauri/src-tauri/vendor/tauri-runtime/src/webview.rs
vendored
Normal file
820
apps/tauri/src-tauri/vendor/tauri-runtime/src/webview.rs
vendored
Normal file
|
|
@ -0,0 +1,820 @@
|
|||
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
//! A layer between raw [`Runtime`] webviews and Tauri.
|
||||
//!
|
||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||
use crate::window::WindowId;
|
||||
use crate::{Rect, Runtime, UserEvent, window::is_label_valid};
|
||||
|
||||
use http::Request;
|
||||
use tauri_utils::config::{
|
||||
BackgroundThrottlingPolicy, Color, ScrollBarStyle as ConfigScrollBarStyle, WebviewUrl,
|
||||
WindowConfig, WindowEffectsConfig,
|
||||
};
|
||||
use url::Url;
|
||||
|
||||
use std::{
|
||||
borrow::Cow,
|
||||
collections::HashMap,
|
||||
hash::{Hash, Hasher},
|
||||
path::PathBuf,
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
pub type UriSchemeProtocolHandler = dyn Fn(&str, http::Request<Vec<u8>>, Box<dyn FnOnce(http::Response<Cow<'static, [u8]>>) + Send>)
|
||||
+ Send
|
||||
+ Sync
|
||||
+ 'static;
|
||||
|
||||
pub type WebResourceRequestHandler =
|
||||
dyn Fn(http::Request<Vec<u8>>, &mut http::Response<Cow<'static, [u8]>>) + Send + Sync;
|
||||
|
||||
pub type NavigationHandler = dyn Fn(&Url) -> bool + Send;
|
||||
|
||||
pub type NewWindowHandler<T, R> =
|
||||
dyn Fn(Url, NewWindowFeatures<T, R>) -> NewWindowResponse + Send + Sync;
|
||||
|
||||
pub type OnPageLoadHandler = dyn Fn(Url, PageLoadEvent) + Send;
|
||||
|
||||
pub type DocumentTitleChangedHandler = dyn Fn(String) + Send + 'static;
|
||||
|
||||
pub type AddressChangedHandler = dyn Fn(&Url) + Send + Sync + 'static;
|
||||
|
||||
pub type DownloadHandler = dyn Fn(DownloadEvent) -> bool + Send + Sync;
|
||||
|
||||
#[cfg(any(target_os = "macos", target_os = "ios"))]
|
||||
type OnWebContentProcessTerminateHandler = dyn Fn() + Send;
|
||||
|
||||
#[cfg(target_os = "ios")]
|
||||
type InputAccessoryViewBuilderFn = dyn Fn(&objc2_ui_kit::UIView) -> Option<objc2::rc::Retained<objc2_ui_kit::UIView>>
|
||||
+ Send
|
||||
+ Sync
|
||||
+ 'static;
|
||||
|
||||
/// Download event.
|
||||
pub enum DownloadEvent<'a> {
|
||||
/// Download requested.
|
||||
Requested {
|
||||
/// The url being downloaded.
|
||||
url: Url,
|
||||
/// Represents where the file will be downloaded to.
|
||||
/// Can be used to set the download location by assigning a new path to it.
|
||||
/// The assigned path _must_ be absolute.
|
||||
destination: &'a mut PathBuf,
|
||||
},
|
||||
/// Download finished.
|
||||
Finished {
|
||||
/// The URL of the original download request.
|
||||
url: Url,
|
||||
/// Potentially representing the filesystem path the file was downloaded to.
|
||||
path: Option<PathBuf>,
|
||||
/// Indicates if the download succeeded or not.
|
||||
success: bool,
|
||||
},
|
||||
}
|
||||
|
||||
#[cfg(target_os = "android")]
|
||||
pub struct CreationContext<'a, 'b> {
|
||||
pub env: &'a mut jni::JNIEnv<'b>,
|
||||
pub activity: &'a jni::objects::JObject<'b>,
|
||||
pub webview: &'a jni::objects::JObject<'b>,
|
||||
}
|
||||
|
||||
/// Kind of event for the page load handler.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum PageLoadEvent {
|
||||
/// Page started to load.
|
||||
Started,
|
||||
/// Page finished loading.
|
||||
Finished,
|
||||
}
|
||||
|
||||
/// Window features of a window requested to open.
|
||||
#[derive(Debug)]
|
||||
pub struct NewWindowFeatures<T: UserEvent, R: Runtime<T>> {
|
||||
pub(crate) size: Option<crate::dpi::LogicalSize<f64>>,
|
||||
pub(crate) position: Option<crate::dpi::LogicalPosition<f64>>,
|
||||
pub(crate) opener: R::WindowOpener,
|
||||
}
|
||||
|
||||
impl<T: UserEvent, R: Runtime<T>> NewWindowFeatures<T, R> {
|
||||
pub fn new(
|
||||
size: Option<crate::dpi::LogicalSize<f64>>,
|
||||
position: Option<crate::dpi::LogicalPosition<f64>>,
|
||||
opener: R::WindowOpener,
|
||||
) -> Self {
|
||||
Self {
|
||||
size,
|
||||
position,
|
||||
opener,
|
||||
}
|
||||
}
|
||||
|
||||
/// Specifies the size of the content area
|
||||
/// as defined by the user's operating system where the new window will be generated.
|
||||
pub fn size(&self) -> Option<crate::dpi::LogicalSize<f64>> {
|
||||
self.size
|
||||
}
|
||||
|
||||
/// Specifies the position of the window relative to the work area
|
||||
/// as defined by the user's operating system where the new window will be generated.
|
||||
pub fn position(&self) -> Option<crate::dpi::LogicalPosition<f64>> {
|
||||
self.position
|
||||
}
|
||||
|
||||
/// Returns information about the webview that initiated a new window request.
|
||||
pub fn opener(&self) -> &R::WindowOpener {
|
||||
&self.opener
|
||||
}
|
||||
|
||||
/// Returns information about the webview that initiated a new window request.
|
||||
pub fn into_opener(self) -> R::WindowOpener {
|
||||
self.opener
|
||||
}
|
||||
}
|
||||
|
||||
/// Response for the new window request handler.
|
||||
pub enum NewWindowResponse {
|
||||
/// Allow the window to be opened with the default implementation.
|
||||
Allow,
|
||||
/// Allow the window to be opened, with the given window.
|
||||
///
|
||||
/// The window must be created referencing the opener window so it can inherit the appropriate attributes.
|
||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||
Create { window_id: WindowId },
|
||||
/// Deny the window from being opened.
|
||||
Deny,
|
||||
}
|
||||
|
||||
/// The scrollbar style to use in the webview.
|
||||
///
|
||||
/// ## Platform-specific
|
||||
///
|
||||
/// - **Windows**: This option must be given the same value for all webviews that target the same data directory.
|
||||
#[non_exhaustive]
|
||||
#[derive(Debug, Clone, Copy, Default)]
|
||||
pub enum ScrollBarStyle {
|
||||
#[default]
|
||||
/// The default scrollbar style for the webview.
|
||||
Default,
|
||||
|
||||
#[cfg(windows)]
|
||||
/// Fluent UI style overlay scrollbars. **Windows Only**
|
||||
///
|
||||
/// Requires WebView2 Runtime version 125.0.2535.41 or higher, does nothing on older versions,
|
||||
/// see <https://learn.microsoft.com/en-us/microsoft-edge/webview2/release-notes/?tabs=dotnetcsharp#10253541>
|
||||
FluentOverlay,
|
||||
}
|
||||
|
||||
/// A webview that has yet to be built.
|
||||
pub struct PendingWebview<T: UserEvent, R: Runtime<T>> {
|
||||
/// The label that the webview will be named.
|
||||
pub label: String,
|
||||
|
||||
/// The [`WebviewAttributes`] that the webview will be created with.
|
||||
pub webview_attributes: WebviewAttributes,
|
||||
|
||||
/// Information about the webview that initiated a new window request.
|
||||
pub opener: Option<R::WindowOpener>,
|
||||
|
||||
/// Runtime specific attributes.
|
||||
pub platform_specific_attributes: Vec<R::PlatformSpecificWebviewAttribute>,
|
||||
|
||||
/// Custom protocols to register on the webview
|
||||
pub uri_scheme_protocols: HashMap<String, Box<UriSchemeProtocolHandler>>,
|
||||
|
||||
/// How to handle IPC calls on the webview.
|
||||
pub ipc_handler: Option<WebviewIpcHandler<T, R>>,
|
||||
|
||||
/// A handler to decide if incoming url is allowed to navigate.
|
||||
pub navigation_handler: Option<Box<NavigationHandler>>,
|
||||
|
||||
pub new_window_handler: Option<Box<NewWindowHandler<T, R>>>,
|
||||
|
||||
pub document_title_changed_handler: Option<Box<DocumentTitleChangedHandler>>,
|
||||
|
||||
pub address_changed_handler: Option<Box<AddressChangedHandler>>,
|
||||
|
||||
/// The resolved URL to load on the webview.
|
||||
pub url: String,
|
||||
|
||||
#[cfg(target_os = "android")]
|
||||
#[allow(clippy::type_complexity)]
|
||||
pub on_webview_created:
|
||||
Option<Box<dyn Fn(CreationContext<'_, '_>) -> Result<(), jni::errors::Error> + Send + Sync>>,
|
||||
|
||||
pub web_resource_request_handler: Option<Box<WebResourceRequestHandler>>,
|
||||
|
||||
pub on_page_load_handler: Option<Box<OnPageLoadHandler>>,
|
||||
|
||||
pub download_handler: Option<Arc<DownloadHandler>>,
|
||||
|
||||
#[cfg(any(target_os = "macos", target_os = "ios"))]
|
||||
pub on_web_content_process_terminate_handler: Option<Box<OnWebContentProcessTerminateHandler>>,
|
||||
}
|
||||
|
||||
impl<T: UserEvent, R: Runtime<T>> PendingWebview<T, R> {
|
||||
/// Create a new [`PendingWebview`] with a label from the given [`WebviewAttributes`].
|
||||
pub fn new(
|
||||
webview_attributes: WebviewAttributes,
|
||||
platform_specific_attributes: Vec<R::PlatformSpecificWebviewAttribute>,
|
||||
label: impl Into<String>,
|
||||
) -> crate::Result<Self> {
|
||||
let label = label.into();
|
||||
if !is_label_valid(&label) {
|
||||
Err(crate::Error::InvalidWindowLabel)
|
||||
} else {
|
||||
Ok(Self {
|
||||
webview_attributes,
|
||||
opener: None,
|
||||
platform_specific_attributes,
|
||||
uri_scheme_protocols: Default::default(),
|
||||
label,
|
||||
ipc_handler: None,
|
||||
navigation_handler: None,
|
||||
new_window_handler: None,
|
||||
document_title_changed_handler: None,
|
||||
address_changed_handler: None,
|
||||
url: "tauri://localhost".to_string(),
|
||||
#[cfg(target_os = "android")]
|
||||
on_webview_created: None,
|
||||
web_resource_request_handler: None,
|
||||
on_page_load_handler: None,
|
||||
download_handler: None,
|
||||
#[cfg(any(target_os = "macos", target_os = "ios"))]
|
||||
on_web_content_process_terminate_handler: None,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub fn register_uri_scheme_protocol<
|
||||
N: Into<String>,
|
||||
H: Fn(&str, http::Request<Vec<u8>>, Box<dyn FnOnce(http::Response<Cow<'static, [u8]>>) + Send>)
|
||||
+ Send
|
||||
+ Sync
|
||||
+ 'static,
|
||||
>(
|
||||
&mut self,
|
||||
uri_scheme: N,
|
||||
protocol_handler: H,
|
||||
) {
|
||||
let uri_scheme = uri_scheme.into();
|
||||
self
|
||||
.uri_scheme_protocols
|
||||
.insert(uri_scheme, Box::new(protocol_handler));
|
||||
}
|
||||
|
||||
#[cfg(target_os = "android")]
|
||||
pub fn on_webview_created<
|
||||
F: Fn(CreationContext<'_, '_>) -> Result<(), jni::errors::Error> + Send + Sync + 'static,
|
||||
>(
|
||||
mut self,
|
||||
f: F,
|
||||
) -> Self {
|
||||
self.on_webview_created.replace(Box::new(f));
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
/// A webview that is not yet managed by Tauri.
|
||||
#[derive(Debug)]
|
||||
pub struct DetachedWebview<T: UserEvent, R: Runtime<T>> {
|
||||
/// Name of the window
|
||||
pub label: String,
|
||||
|
||||
/// The [`crate::WebviewDispatch`] associated with the window.
|
||||
pub dispatcher: R::WebviewDispatcher,
|
||||
}
|
||||
|
||||
impl<T: UserEvent, R: Runtime<T>> Clone for DetachedWebview<T, R> {
|
||||
fn clone(&self) -> Self {
|
||||
Self {
|
||||
label: self.label.clone(),
|
||||
dispatcher: self.dispatcher.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: UserEvent, R: Runtime<T>> Hash for DetachedWebview<T, R> {
|
||||
/// Only use the [`DetachedWebview`]'s label to represent its hash.
|
||||
fn hash<H: Hasher>(&self, state: &mut H) {
|
||||
self.label.hash(state)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: UserEvent, R: Runtime<T>> Eq for DetachedWebview<T, R> {}
|
||||
impl<T: UserEvent, R: Runtime<T>> PartialEq for DetachedWebview<T, R> {
|
||||
/// Only use the [`DetachedWebview`]'s label to compare equality.
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.label.eq(&other.label)
|
||||
}
|
||||
}
|
||||
|
||||
/// The attributes used to create an webview.
|
||||
#[derive(Debug)]
|
||||
pub struct WebviewAttributes {
|
||||
pub url: WebviewUrl,
|
||||
pub user_agent: Option<String>,
|
||||
/// A list of initialization javascript scripts to run when loading new pages.
|
||||
/// When webview load a new page, this initialization code will be executed.
|
||||
/// It is guaranteed that code is executed before `window.onload`.
|
||||
///
|
||||
/// ## Platform-specific
|
||||
///
|
||||
/// - **Windows:** scripts are always added to subframes.
|
||||
/// - **Android:** When [addDocumentStartJavaScript] is not supported,
|
||||
/// we prepend initialization scripts to each HTML head (implementation only supported on custom protocol URLs).
|
||||
/// For remote URLs, we use [onPageStarted] which is not guaranteed to run before other scripts.
|
||||
///
|
||||
/// [addDocumentStartJavaScript]: https://developer.android.com/reference/androidx/webkit/WebViewCompat#addDocumentStartJavaScript(android.webkit.WebView,java.lang.String,java.util.Set%3Cjava.lang.String%3E)
|
||||
/// [onPageStarted]: https://developer.android.com/reference/android/webkit/WebViewClient#onPageStarted(android.webkit.WebView,%20java.lang.String,%20android.graphics.Bitmap)
|
||||
pub initialization_scripts: Vec<InitializationScript>,
|
||||
pub data_directory: Option<PathBuf>,
|
||||
pub drag_drop_handler_enabled: bool,
|
||||
pub clipboard: bool,
|
||||
pub accept_first_mouse: bool,
|
||||
pub additional_browser_args: Option<String>,
|
||||
pub window_effects: Option<WindowEffectsConfig>,
|
||||
pub incognito: bool,
|
||||
pub transparent: bool,
|
||||
pub focus: bool,
|
||||
pub bounds: Option<Rect>,
|
||||
pub auto_resize: bool,
|
||||
pub proxy_url: Option<Url>,
|
||||
pub zoom_hotkeys_enabled: bool,
|
||||
pub browser_extensions_enabled: bool,
|
||||
pub extensions_path: Option<PathBuf>,
|
||||
pub data_store_identifier: Option<[u8; 16]>,
|
||||
pub use_https_scheme: bool,
|
||||
pub devtools: Option<bool>,
|
||||
pub background_color: Option<Color>,
|
||||
pub traffic_light_position: Option<dpi::Position>,
|
||||
pub background_throttling: Option<BackgroundThrottlingPolicy>,
|
||||
pub javascript_disabled: bool,
|
||||
/// on macOS and iOS there is a link preview on long pressing links, this is enabled by default.
|
||||
/// see https://docs.rs/objc2-web-kit/latest/objc2_web_kit/struct.WKWebView.html#method.allowsLinkPreview
|
||||
pub allow_link_preview: bool,
|
||||
pub scroll_bar_style: ScrollBarStyle,
|
||||
/// Controls the WebView's browser-level general autofill behavior.
|
||||
///
|
||||
/// **This option does not disable password or credit card autofill.**
|
||||
///
|
||||
/// When set to `false`, the WebView will not automatically populate
|
||||
/// general form fields using previously stored data such as addresses
|
||||
/// or contact information.
|
||||
///
|
||||
/// If not specified, this is `true` by default.
|
||||
///
|
||||
/// ## Platform-specific
|
||||
///
|
||||
/// - **Windows**: Supported. WebView2's autofill feature (called
|
||||
/// "Suggestions") may not honor `autocomplete="off"` on input
|
||||
/// elements in some cases.
|
||||
/// - **Linux / Android / iOS / macOS**: Unsupported and performs no
|
||||
/// operation.
|
||||
pub general_autofill_enabled: bool,
|
||||
/// Allows overriding the keyboard accessory view on iOS.
|
||||
/// Returning `None` effectively removes the view.
|
||||
///
|
||||
/// The closure parameter is the webview instance.
|
||||
///
|
||||
/// The accessory view is the view that appears above the keyboard when a text input element is focused.
|
||||
/// It usually displays a view with "Done", "Next" buttons.
|
||||
///
|
||||
/// # Stability
|
||||
///
|
||||
/// This relies on [`objc2_ui_kit`] which does not provide a stable API yet, so it can receive breaking changes in minor releases.
|
||||
#[cfg(target_os = "ios")]
|
||||
pub input_accessory_view_builder: Option<InputAccessoryViewBuilder>,
|
||||
}
|
||||
|
||||
unsafe impl Send for WebviewAttributes {}
|
||||
unsafe impl Sync for WebviewAttributes {}
|
||||
|
||||
#[cfg(target_os = "ios")]
|
||||
#[non_exhaustive]
|
||||
pub struct InputAccessoryViewBuilder(pub Box<InputAccessoryViewBuilderFn>);
|
||||
|
||||
#[cfg(target_os = "ios")]
|
||||
impl std::fmt::Debug for InputAccessoryViewBuilder {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::result::Result<(), std::fmt::Error> {
|
||||
f.debug_struct("InputAccessoryViewBuilder").finish()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "ios")]
|
||||
impl InputAccessoryViewBuilder {
|
||||
pub fn new(builder: Box<InputAccessoryViewBuilderFn>) -> Self {
|
||||
Self(builder)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&WindowConfig> for WebviewAttributes {
|
||||
fn from(config: &WindowConfig) -> Self {
|
||||
let mut builder = Self::new(config.url.clone())
|
||||
.incognito(config.incognito)
|
||||
.focused(config.focus)
|
||||
.zoom_hotkeys_enabled(config.zoom_hotkeys_enabled)
|
||||
.use_https_scheme(config.use_https_scheme)
|
||||
.browser_extensions_enabled(config.browser_extensions_enabled)
|
||||
.background_throttling(config.background_throttling.clone())
|
||||
.devtools(config.devtools)
|
||||
.scroll_bar_style(match config.scroll_bar_style {
|
||||
ConfigScrollBarStyle::Default => ScrollBarStyle::Default,
|
||||
#[cfg(windows)]
|
||||
ConfigScrollBarStyle::FluentOverlay => ScrollBarStyle::FluentOverlay,
|
||||
_ => ScrollBarStyle::Default,
|
||||
})
|
||||
.general_autofill_enabled(config.general_autofill_enabled);
|
||||
|
||||
#[cfg(any(not(target_os = "macos"), feature = "macos-private-api"))]
|
||||
{
|
||||
builder = builder.transparent(config.transparent);
|
||||
}
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
if let Some(position) = &config.traffic_light_position {
|
||||
builder =
|
||||
builder.traffic_light_position(dpi::LogicalPosition::new(position.x, position.y).into());
|
||||
}
|
||||
}
|
||||
builder = builder.accept_first_mouse(config.accept_first_mouse);
|
||||
if !config.drag_drop_enabled {
|
||||
builder = builder.disable_drag_drop_handler();
|
||||
}
|
||||
if let Some(user_agent) = &config.user_agent {
|
||||
builder = builder.user_agent(user_agent);
|
||||
}
|
||||
if let Some(additional_browser_args) = &config.additional_browser_args {
|
||||
builder = builder.additional_browser_args(additional_browser_args);
|
||||
}
|
||||
if let Some(effects) = &config.window_effects {
|
||||
builder = builder.window_effects(effects.clone());
|
||||
}
|
||||
if let Some(url) = &config.proxy_url {
|
||||
builder = builder.proxy_url(url.to_owned());
|
||||
}
|
||||
if let Some(color) = config.background_color {
|
||||
builder = builder.background_color(color);
|
||||
}
|
||||
builder.javascript_disabled = config.javascript_disabled;
|
||||
builder.allow_link_preview = config.allow_link_preview;
|
||||
#[cfg(target_os = "ios")]
|
||||
if config.disable_input_accessory_view {
|
||||
builder
|
||||
.input_accessory_view_builder
|
||||
.replace(InputAccessoryViewBuilder::new(Box::new(|_webview| None)));
|
||||
}
|
||||
builder
|
||||
}
|
||||
}
|
||||
|
||||
impl WebviewAttributes {
|
||||
/// Initializes the default attributes for a webview.
|
||||
pub fn new(url: WebviewUrl) -> Self {
|
||||
Self {
|
||||
url,
|
||||
user_agent: None,
|
||||
initialization_scripts: Vec::new(),
|
||||
data_directory: None,
|
||||
drag_drop_handler_enabled: true,
|
||||
clipboard: false,
|
||||
accept_first_mouse: false,
|
||||
additional_browser_args: None,
|
||||
window_effects: None,
|
||||
incognito: false,
|
||||
transparent: false,
|
||||
focus: true,
|
||||
bounds: None,
|
||||
auto_resize: false,
|
||||
proxy_url: None,
|
||||
zoom_hotkeys_enabled: false,
|
||||
browser_extensions_enabled: false,
|
||||
data_store_identifier: None,
|
||||
extensions_path: None,
|
||||
use_https_scheme: false,
|
||||
devtools: None,
|
||||
background_color: None,
|
||||
traffic_light_position: None,
|
||||
background_throttling: None,
|
||||
javascript_disabled: false,
|
||||
allow_link_preview: true,
|
||||
scroll_bar_style: ScrollBarStyle::Default,
|
||||
general_autofill_enabled: true,
|
||||
#[cfg(target_os = "ios")]
|
||||
input_accessory_view_builder: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets the user agent
|
||||
#[must_use]
|
||||
pub fn user_agent(mut self, user_agent: &str) -> Self {
|
||||
self.user_agent = Some(user_agent.to_string());
|
||||
self
|
||||
}
|
||||
|
||||
/// Adds an init script for the main frame.
|
||||
///
|
||||
/// When webview load a new page, this initialization code will be executed.
|
||||
/// It is guaranteed that code is executed before `window.onload`.
|
||||
///
|
||||
/// This is executed only on the main frame.
|
||||
/// If you only want to run it in all frames, use [`Self::initialization_script_on_all_frames`] instead.
|
||||
///
|
||||
/// ## Platform-specific
|
||||
///
|
||||
/// - **Windows:** scripts are always added to subframes.
|
||||
/// - **Android:** When [addDocumentStartJavaScript] is not supported,
|
||||
/// we prepend initialization scripts to each HTML head (implementation only supported on custom protocol URLs).
|
||||
/// For remote URLs, we use [onPageStarted] which is not guaranteed to run before other scripts.
|
||||
///
|
||||
/// [addDocumentStartJavaScript]: https://developer.android.com/reference/androidx/webkit/WebViewCompat#addDocumentStartJavaScript(android.webkit.WebView,java.lang.String,java.util.Set%3Cjava.lang.String%3E)
|
||||
/// [onPageStarted]: https://developer.android.com/reference/android/webkit/WebViewClient#onPageStarted(android.webkit.WebView,%20java.lang.String,%20android.graphics.Bitmap)
|
||||
#[must_use]
|
||||
pub fn initialization_script(mut self, script: impl Into<String>) -> Self {
|
||||
self.initialization_scripts.push(InitializationScript {
|
||||
script: script.into(),
|
||||
for_main_frame_only: true,
|
||||
});
|
||||
self
|
||||
}
|
||||
|
||||
/// Adds an init script for all frames.
|
||||
///
|
||||
/// When webview load a new page, this initialization code will be executed.
|
||||
/// It is guaranteed that code is executed before `window.onload`.
|
||||
///
|
||||
/// This is executed on all frames, main frame and also sub frames.
|
||||
/// If you only want to run it in the main frame, use [`Self::initialization_script`] instead.
|
||||
///
|
||||
/// ## Platform-specific
|
||||
///
|
||||
/// - **Windows:** scripts are always added to subframes.
|
||||
/// - **Android:** When [addDocumentStartJavaScript] is not supported,
|
||||
/// we prepend initialization scripts to each HTML head (implementation only supported on custom protocol URLs).
|
||||
/// For remote URLs, we use [onPageStarted] which is not guaranteed to run before other scripts.
|
||||
///
|
||||
/// [addDocumentStartJavaScript]: https://developer.android.com/reference/androidx/webkit/WebViewCompat#addDocumentStartJavaScript(android.webkit.WebView,java.lang.String,java.util.Set%3Cjava.lang.String%3E)
|
||||
/// [onPageStarted]: https://developer.android.com/reference/android/webkit/WebViewClient#onPageStarted(android.webkit.WebView,%20java.lang.String,%20android.graphics.Bitmap)
|
||||
#[must_use]
|
||||
pub fn initialization_script_on_all_frames(mut self, script: impl Into<String>) -> Self {
|
||||
self.initialization_scripts.push(InitializationScript {
|
||||
script: script.into(),
|
||||
for_main_frame_only: false,
|
||||
});
|
||||
self
|
||||
}
|
||||
|
||||
/// Data directory for the webview.
|
||||
#[must_use]
|
||||
pub fn data_directory(mut self, data_directory: PathBuf) -> Self {
|
||||
self.data_directory.replace(data_directory);
|
||||
self
|
||||
}
|
||||
|
||||
/// Disables the drag and drop handler. This is required to use HTML5 drag and drop APIs on the frontend on Windows.
|
||||
#[must_use]
|
||||
pub fn disable_drag_drop_handler(mut self) -> Self {
|
||||
self.drag_drop_handler_enabled = false;
|
||||
self
|
||||
}
|
||||
|
||||
/// Enables clipboard access for the page rendered on **Linux** and **Windows**.
|
||||
///
|
||||
/// **macOS** doesn't provide such method and is always enabled by default,
|
||||
/// but you still need to add menu item accelerators to use shortcuts.
|
||||
#[must_use]
|
||||
pub fn enable_clipboard_access(mut self) -> Self {
|
||||
self.clipboard = true;
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets whether clicking an inactive window also clicks through to the webview.
|
||||
#[must_use]
|
||||
pub fn accept_first_mouse(mut self, accept: bool) -> Self {
|
||||
self.accept_first_mouse = accept;
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets additional browser arguments. **Windows Only**
|
||||
#[must_use]
|
||||
pub fn additional_browser_args(mut self, additional_args: &str) -> Self {
|
||||
self.additional_browser_args = Some(additional_args.to_string());
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets window effects
|
||||
#[must_use]
|
||||
pub fn window_effects(mut self, effects: WindowEffectsConfig) -> Self {
|
||||
self.window_effects = Some(effects);
|
||||
self
|
||||
}
|
||||
|
||||
/// Enable or disable incognito mode for the WebView.
|
||||
#[must_use]
|
||||
pub fn incognito(mut self, incognito: bool) -> Self {
|
||||
self.incognito = incognito;
|
||||
self
|
||||
}
|
||||
|
||||
/// Enable or disable transparency for the WebView.
|
||||
#[cfg(any(not(target_os = "macos"), feature = "macos-private-api"))]
|
||||
#[must_use]
|
||||
pub fn transparent(mut self, transparent: bool) -> Self {
|
||||
self.transparent = transparent;
|
||||
self
|
||||
}
|
||||
|
||||
/// Whether the webview should be focused or not.
|
||||
#[must_use]
|
||||
pub fn focused(mut self, focus: bool) -> Self {
|
||||
self.focus = focus;
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the webview to automatically grow and shrink its size and position when the parent window resizes.
|
||||
#[must_use]
|
||||
pub fn auto_resize(mut self) -> Self {
|
||||
self.auto_resize = true;
|
||||
self
|
||||
}
|
||||
|
||||
/// Enable proxy for the WebView
|
||||
#[must_use]
|
||||
pub fn proxy_url(mut self, url: Url) -> Self {
|
||||
self.proxy_url = Some(url);
|
||||
self
|
||||
}
|
||||
|
||||
/// Whether page zooming by hotkeys is enabled
|
||||
///
|
||||
/// ## Platform-specific:
|
||||
///
|
||||
/// - **Windows**: Controls WebView2's [`IsZoomControlEnabled`](https://learn.microsoft.com/en-us/microsoft-edge/webview2/reference/winrt/microsoft_web_webview2_core/corewebview2settings?view=webview2-winrt-1.0.2420.47#iszoomcontrolenabled) setting.
|
||||
/// - **MacOS / Linux**: Injects a polyfill that zooms in and out with `ctrl/command` + `-/=`,
|
||||
/// 20% in each step, ranging from 20% to 1000%. Requires `webview:allow-set-webview-zoom` permission
|
||||
///
|
||||
/// - **Android / iOS**: Unsupported.
|
||||
#[must_use]
|
||||
pub fn zoom_hotkeys_enabled(mut self, enabled: bool) -> Self {
|
||||
self.zoom_hotkeys_enabled = enabled;
|
||||
self
|
||||
}
|
||||
|
||||
/// Whether browser extensions can be installed for the webview process
|
||||
///
|
||||
/// ## Platform-specific:
|
||||
///
|
||||
/// - **Windows**: Enables the WebView2 environment's [`AreBrowserExtensionsEnabled`](https://learn.microsoft.com/en-us/microsoft-edge/webview2/reference/winrt/microsoft_web_webview2_core/corewebview2environmentoptions?view=webview2-winrt-1.0.2739.15#arebrowserextensionsenabled)
|
||||
/// - **MacOS / Linux / iOS / Android** - Unsupported.
|
||||
#[must_use]
|
||||
pub fn browser_extensions_enabled(mut self, enabled: bool) -> Self {
|
||||
self.browser_extensions_enabled = enabled;
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets whether the custom protocols should use `https://<scheme>.localhost` instead of the default `http://<scheme>.localhost` on Windows and Android. Defaults to `false`.
|
||||
///
|
||||
/// ## Note
|
||||
///
|
||||
/// Using a `https` scheme will NOT allow mixed content when trying to fetch `http` endpoints and therefore will not match the behavior of the `<scheme>://localhost` protocols used on macOS and Linux.
|
||||
///
|
||||
/// ## Warning
|
||||
///
|
||||
/// Changing this value between releases will change the IndexedDB, cookies and localstorage location and your app will not be able to access the old data.
|
||||
#[must_use]
|
||||
pub fn use_https_scheme(mut self, enabled: bool) -> Self {
|
||||
self.use_https_scheme = enabled;
|
||||
self
|
||||
}
|
||||
|
||||
/// Whether web inspector, which is usually called browser devtools, is enabled or not. Enabled by default.
|
||||
///
|
||||
/// This API works in **debug** builds, but requires `devtools` feature flag to enable it in **release** builds.
|
||||
///
|
||||
/// ## Platform-specific
|
||||
///
|
||||
/// - macOS: This will call private functions on **macOS**.
|
||||
/// - Android: Open `chrome://inspect/#devices` in Chrome to get the devtools window. Wry's `WebView` devtools API isn't supported on Android.
|
||||
/// - iOS: Open Safari > Develop > [Your Device Name] > [Your WebView] to get the devtools window.
|
||||
#[must_use]
|
||||
pub fn devtools(mut self, enabled: Option<bool>) -> Self {
|
||||
self.devtools = enabled;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the window and webview background color.
|
||||
/// ## Platform-specific:
|
||||
///
|
||||
/// - **Windows**: On Windows 7, alpha channel is ignored for the webview layer.
|
||||
/// - **Windows**: On Windows 8 and newer, if alpha channel is not `0`, it will be ignored.
|
||||
#[must_use]
|
||||
pub fn background_color(mut self, color: Color) -> Self {
|
||||
self.background_color = Some(color);
|
||||
self
|
||||
}
|
||||
|
||||
/// Change the position of the window controls. Available on macOS only.
|
||||
///
|
||||
/// Requires titleBarStyle: Overlay and decorations: true.
|
||||
///
|
||||
/// ## Platform-specific
|
||||
///
|
||||
/// - **Linux / Windows / iOS / Android:** Unsupported.
|
||||
#[must_use]
|
||||
pub fn traffic_light_position(mut self, position: dpi::Position) -> Self {
|
||||
self.traffic_light_position = Some(position);
|
||||
self
|
||||
}
|
||||
|
||||
/// Whether to show a link preview when long pressing on links. Available on macOS and iOS only.
|
||||
///
|
||||
/// Default is true.
|
||||
///
|
||||
/// See https://docs.rs/objc2-web-kit/latest/objc2_web_kit/struct.WKWebView.html#method.allowsLinkPreview
|
||||
///
|
||||
/// ## Platform-specific
|
||||
///
|
||||
/// - **Linux / Windows / Android:** Unsupported.
|
||||
#[must_use]
|
||||
pub fn allow_link_preview(mut self, allow_link_preview: bool) -> Self {
|
||||
self.allow_link_preview = allow_link_preview;
|
||||
self
|
||||
}
|
||||
|
||||
/// Change the default background throttling behavior.
|
||||
///
|
||||
/// By default, browsers use a suspend policy that will throttle timers and even unload
|
||||
/// the whole tab (view) to free resources after roughly 5 minutes when a view became
|
||||
/// minimized or hidden. This will pause all tasks until the documents visibility state
|
||||
/// changes back from hidden to visible by bringing the view back to the foreground.
|
||||
///
|
||||
/// ## Platform-specific
|
||||
///
|
||||
/// - **Linux / Windows / Android**: Unsupported. Workarounds like a pending WebLock transaction might suffice.
|
||||
/// - **iOS**: Supported since version 17.0+.
|
||||
/// - **macOS**: Supported since version 14.0+.
|
||||
///
|
||||
/// see <https://github.com/tauri-apps/tauri/issues/5250#issuecomment-2569380578>
|
||||
#[must_use]
|
||||
pub fn background_throttling(mut self, policy: Option<BackgroundThrottlingPolicy>) -> Self {
|
||||
self.background_throttling = policy;
|
||||
self
|
||||
}
|
||||
|
||||
/// Specifies the native scrollbar style to use with the webview.
|
||||
/// CSS styles that modify the scrollbar are applied on top of the native appearance configured here.
|
||||
///
|
||||
/// Defaults to [`ScrollBarStyle::Default`], which is the browser default.
|
||||
///
|
||||
/// ## Platform-specific
|
||||
///
|
||||
/// - **Windows**:
|
||||
/// - [`ScrollBarStyle::FluentOverlay`] requires WebView2 Runtime version 125.0.2535.41 or higher,
|
||||
/// and does nothing on older versions.
|
||||
/// - This option must be given the same value for all webviews that target the same data directory. Use
|
||||
/// [`WebviewAttributes::data_directory`] to change data directories if needed.
|
||||
/// - **Linux / Android / iOS / macOS**: Unsupported. Only supports `Default` and performs no operation.
|
||||
#[must_use]
|
||||
pub fn scroll_bar_style(mut self, style: ScrollBarStyle) -> Self {
|
||||
self.scroll_bar_style = style;
|
||||
self
|
||||
}
|
||||
|
||||
/// Controls the WebView's browser-level general autofill behavior.
|
||||
///
|
||||
/// **This option does not disable password or credit card autofill.**
|
||||
///
|
||||
/// When set to `false`, the WebView will not automatically populate
|
||||
/// general form fields using previously stored data such as addresses
|
||||
/// or contact information.
|
||||
///
|
||||
/// By default, this is `true`.
|
||||
///
|
||||
/// ## Platform-specific
|
||||
///
|
||||
/// - **Windows**: Supported. WebView2's autofill feature (called
|
||||
/// "Suggestions") may not honor `autocomplete="off"` on input
|
||||
/// elements in some cases.
|
||||
/// - **Linux / Android / iOS / macOS**: Unsupported and performs no
|
||||
/// operation.
|
||||
#[must_use]
|
||||
pub fn general_autofill_enabled(mut self, enabled: bool) -> Self {
|
||||
self.general_autofill_enabled = enabled;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
/// IPC handler.
|
||||
pub type WebviewIpcHandler<T, R> = Box<dyn Fn(DetachedWebview<T, R>, Request<String>) + Send>;
|
||||
|
||||
/// An initialization script
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct InitializationScript {
|
||||
/// The script to run
|
||||
pub script: String,
|
||||
/// Whether the script should be injected to main frame only
|
||||
pub for_main_frame_only: bool,
|
||||
}
|
||||
661
apps/tauri/src-tauri/vendor/tauri-runtime/src/window.rs
vendored
Normal file
661
apps/tauri/src-tauri/vendor/tauri-runtime/src/window.rs
vendored
Normal file
|
|
@ -0,0 +1,661 @@
|
|||
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
//! A layer between raw [`Runtime`] windows and Tauri.
|
||||
|
||||
use crate::{
|
||||
Icon, Runtime, UserEvent, WindowDispatch,
|
||||
webview::{DetachedWebview, PendingWebview},
|
||||
};
|
||||
|
||||
use dpi::PixelUnit;
|
||||
use serde::{Deserialize, Deserializer, Serialize};
|
||||
use tauri_utils::{
|
||||
Theme,
|
||||
config::{Color, WindowConfig},
|
||||
};
|
||||
#[cfg(windows)]
|
||||
use windows::Win32::Foundation::HWND;
|
||||
|
||||
use std::{
|
||||
hash::{Hash, Hasher},
|
||||
marker::PhantomData,
|
||||
path::PathBuf,
|
||||
sync::mpsc::Sender,
|
||||
};
|
||||
|
||||
/// An event from a window.
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum WindowEvent {
|
||||
/// The size of the window has changed. Contains the client area's new dimensions.
|
||||
Resized(dpi::PhysicalSize<u32>),
|
||||
/// The position of the window has changed. Contains the window's new position.
|
||||
Moved(dpi::PhysicalPosition<i32>),
|
||||
/// The window has been requested to close.
|
||||
CloseRequested {
|
||||
/// A signal sender. If a `true` value is emitted, the window won't be closed.
|
||||
signal_tx: Sender<bool>,
|
||||
},
|
||||
/// The window has been destroyed.
|
||||
Destroyed,
|
||||
/// The window gained or lost focus.
|
||||
///
|
||||
/// The parameter is true if the window has gained focus, and false if it has lost focus.
|
||||
Focused(bool),
|
||||
/// The window's scale factor has changed.
|
||||
///
|
||||
/// The following user actions can cause DPI changes:
|
||||
///
|
||||
/// - Changing the display's resolution.
|
||||
/// - Changing the display's scale factor (e.g. in Control Panel on Windows).
|
||||
/// - Moving the window to a display with a different scale factor.
|
||||
ScaleFactorChanged {
|
||||
/// The new scale factor.
|
||||
scale_factor: f64,
|
||||
/// The window inner size.
|
||||
new_inner_size: dpi::PhysicalSize<u32>,
|
||||
},
|
||||
/// An event associated with the drag and drop action.
|
||||
DragDrop(DragDropEvent),
|
||||
/// The system window theme has changed.
|
||||
///
|
||||
/// Applications might wish to react to this to change the theme of the content of the window when the system changes the window theme.
|
||||
ThemeChanged(Theme),
|
||||
|
||||
/// Emitted when the application has been suspended.
|
||||
///
|
||||
/// ## Platform-specific
|
||||
///
|
||||
/// - **Android**: This is triggered by `onPause` method of the Activity.
|
||||
/// - **iOS**: This is triggered by `applicationWillResignActive` method of the UIApplicationDelegate.
|
||||
/// - **Linux / macOS / Windows**: Unsupported.
|
||||
#[cfg(mobile)]
|
||||
Suspended,
|
||||
|
||||
/// Emitted when the application has been resumed.
|
||||
///
|
||||
/// ## Platform-specific
|
||||
///
|
||||
/// - **Android**: This is triggered by `onResume` method of the Activity. The first onResume() is ignored to match the iOS implementation, since that is called on activity creation.
|
||||
/// - **iOS**: This is triggered by `applicationWillEnterForeground` method of the UIApplicationDelegate.
|
||||
/// - **Linux / macOS / Windows**: Unsupported.
|
||||
#[cfg(mobile)]
|
||||
Resumed,
|
||||
}
|
||||
|
||||
/// An event from a window.
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum WebviewEvent {
|
||||
/// An event associated with the drag and drop action.
|
||||
DragDrop(DragDropEvent),
|
||||
}
|
||||
|
||||
/// The drag drop event payload.
|
||||
#[derive(Debug, Clone)]
|
||||
#[non_exhaustive]
|
||||
pub enum DragDropEvent {
|
||||
/// A drag operation has entered the webview.
|
||||
Enter {
|
||||
/// List of paths that are being dragged onto the webview.
|
||||
paths: Vec<PathBuf>,
|
||||
/// The position of the mouse cursor.
|
||||
position: dpi::PhysicalPosition<f64>,
|
||||
},
|
||||
/// A drag operation is moving over the webview.
|
||||
Over {
|
||||
/// The position of the mouse cursor.
|
||||
position: dpi::PhysicalPosition<f64>,
|
||||
},
|
||||
/// The file(s) have been dropped onto the webview.
|
||||
Drop {
|
||||
/// List of paths that are being dropped onto the window.
|
||||
paths: Vec<PathBuf>,
|
||||
/// The position of the mouse cursor.
|
||||
position: dpi::PhysicalPosition<f64>,
|
||||
},
|
||||
/// The drag operation has been cancelled or left the window.
|
||||
Leave,
|
||||
}
|
||||
|
||||
/// Describes the appearance of the mouse cursor.
|
||||
#[non_exhaustive]
|
||||
#[derive(Debug, Default, Copy, Clone, PartialEq, Eq, Hash)]
|
||||
pub enum CursorIcon {
|
||||
/// The platform-dependent default cursor.
|
||||
#[default]
|
||||
Default,
|
||||
/// A simple crosshair.
|
||||
Crosshair,
|
||||
/// A hand (often used to indicate links in web browsers).
|
||||
Hand,
|
||||
/// Self explanatory.
|
||||
Arrow,
|
||||
/// Indicates something is to be moved.
|
||||
Move,
|
||||
/// Indicates text that may be selected or edited.
|
||||
Text,
|
||||
/// Program busy indicator.
|
||||
Wait,
|
||||
/// Help indicator (often rendered as a "?")
|
||||
Help,
|
||||
/// Progress indicator. Shows that processing is being done. But in contrast
|
||||
/// with "Wait" the user may still interact with the program. Often rendered
|
||||
/// as a spinning beach ball, or an arrow with a watch or hourglass.
|
||||
Progress,
|
||||
|
||||
/// Cursor showing that something cannot be done.
|
||||
NotAllowed,
|
||||
ContextMenu,
|
||||
Cell,
|
||||
VerticalText,
|
||||
Alias,
|
||||
Copy,
|
||||
NoDrop,
|
||||
/// Indicates something can be grabbed.
|
||||
Grab,
|
||||
/// Indicates something is grabbed.
|
||||
Grabbing,
|
||||
AllScroll,
|
||||
ZoomIn,
|
||||
ZoomOut,
|
||||
|
||||
/// Indicate that some edge is to be moved. For example, the 'SeResize' cursor
|
||||
/// is used when the movement starts from the south-east corner of the box.
|
||||
EResize,
|
||||
NResize,
|
||||
NeResize,
|
||||
NwResize,
|
||||
SResize,
|
||||
SeResize,
|
||||
SwResize,
|
||||
WResize,
|
||||
EwResize,
|
||||
NsResize,
|
||||
NeswResize,
|
||||
NwseResize,
|
||||
ColResize,
|
||||
RowResize,
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for CursorIcon {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
let s = String::deserialize(deserializer)?;
|
||||
Ok(match s.to_lowercase().as_str() {
|
||||
"default" => CursorIcon::Default,
|
||||
"crosshair" => CursorIcon::Crosshair,
|
||||
"hand" => CursorIcon::Hand,
|
||||
"arrow" => CursorIcon::Arrow,
|
||||
"move" => CursorIcon::Move,
|
||||
"text" => CursorIcon::Text,
|
||||
"wait" => CursorIcon::Wait,
|
||||
"help" => CursorIcon::Help,
|
||||
"progress" => CursorIcon::Progress,
|
||||
"notallowed" => CursorIcon::NotAllowed,
|
||||
"contextmenu" => CursorIcon::ContextMenu,
|
||||
"cell" => CursorIcon::Cell,
|
||||
"verticaltext" => CursorIcon::VerticalText,
|
||||
"alias" => CursorIcon::Alias,
|
||||
"copy" => CursorIcon::Copy,
|
||||
"nodrop" => CursorIcon::NoDrop,
|
||||
"grab" => CursorIcon::Grab,
|
||||
"grabbing" => CursorIcon::Grabbing,
|
||||
"allscroll" => CursorIcon::AllScroll,
|
||||
"zoomin" => CursorIcon::ZoomIn,
|
||||
"zoomout" => CursorIcon::ZoomOut,
|
||||
"eresize" => CursorIcon::EResize,
|
||||
"nresize" => CursorIcon::NResize,
|
||||
"neresize" => CursorIcon::NeResize,
|
||||
"nwresize" => CursorIcon::NwResize,
|
||||
"sresize" => CursorIcon::SResize,
|
||||
"seresize" => CursorIcon::SeResize,
|
||||
"swresize" => CursorIcon::SwResize,
|
||||
"wresize" => CursorIcon::WResize,
|
||||
"ewresize" => CursorIcon::EwResize,
|
||||
"nsresize" => CursorIcon::NsResize,
|
||||
"neswresize" => CursorIcon::NeswResize,
|
||||
"nwseresize" => CursorIcon::NwseResize,
|
||||
"colresize" => CursorIcon::ColResize,
|
||||
"rowresize" => CursorIcon::RowResize,
|
||||
_ => CursorIcon::Default,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Window size constraints
|
||||
#[derive(Clone, Copy, PartialEq, Debug, Default, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct WindowSizeConstraints {
|
||||
/// The minimum width a window can be, If this is `None`, the window will have no minimum width.
|
||||
///
|
||||
/// The default is `None`.
|
||||
pub min_width: Option<PixelUnit>,
|
||||
/// The minimum height a window can be, If this is `None`, the window will have no minimum height.
|
||||
///
|
||||
/// The default is `None`.
|
||||
pub min_height: Option<PixelUnit>,
|
||||
/// The maximum width a window can be, If this is `None`, the window will have no maximum width.
|
||||
///
|
||||
/// The default is `None`.
|
||||
pub max_width: Option<PixelUnit>,
|
||||
/// The maximum height a window can be, If this is `None`, the window will have no maximum height.
|
||||
///
|
||||
/// The default is `None`.
|
||||
pub max_height: Option<PixelUnit>,
|
||||
}
|
||||
|
||||
/// Do **NOT** implement this trait except for use in a custom [`Runtime`]
|
||||
///
|
||||
/// This trait is separate from [`WindowBuilder`] to prevent "accidental" implementation.
|
||||
pub trait WindowBuilderBase: std::fmt::Debug + Clone + Sized {}
|
||||
|
||||
/// A builder for all attributes related to a single window.
|
||||
///
|
||||
/// This trait is only meant to be implemented by a custom [`Runtime`]
|
||||
/// and not by applications.
|
||||
pub trait WindowBuilder: WindowBuilderBase {
|
||||
/// Initializes a new window attributes builder.
|
||||
fn new() -> Self;
|
||||
|
||||
/// Initializes a new window builder from a [`WindowConfig`]
|
||||
fn with_config(config: &WindowConfig) -> Self;
|
||||
|
||||
/// Show window in the center of the screen.
|
||||
#[must_use]
|
||||
fn center(self) -> Self;
|
||||
|
||||
/// The initial position of the window in logical pixels.
|
||||
#[must_use]
|
||||
fn position(self, x: f64, y: f64) -> Self;
|
||||
|
||||
/// Window size in logical pixels.
|
||||
#[must_use]
|
||||
fn inner_size(self, width: f64, height: f64) -> Self;
|
||||
|
||||
/// Window min inner size in logical pixels.
|
||||
#[must_use]
|
||||
fn min_inner_size(self, min_width: f64, min_height: f64) -> Self;
|
||||
|
||||
/// Window max inner size in logical pixels.
|
||||
#[must_use]
|
||||
fn max_inner_size(self, max_width: f64, max_height: f64) -> Self;
|
||||
|
||||
/// Window inner size constraints.
|
||||
#[must_use]
|
||||
fn inner_size_constraints(self, constraints: WindowSizeConstraints) -> Self;
|
||||
|
||||
/// Prevent the window from overflowing the working area (e.g. monitor size - taskbar size) on creation
|
||||
///
|
||||
/// ## Platform-specific
|
||||
///
|
||||
/// - **iOS / Android:** Unsupported.
|
||||
#[must_use]
|
||||
fn prevent_overflow(self) -> Self;
|
||||
|
||||
/// Prevent the window from overflowing the working area (e.g. monitor size - taskbar size)
|
||||
/// on creation with a margin
|
||||
///
|
||||
/// ## Platform-specific
|
||||
///
|
||||
/// - **iOS / Android:** Unsupported.
|
||||
#[must_use]
|
||||
fn prevent_overflow_with_margin(self, margin: dpi::Size) -> Self;
|
||||
|
||||
/// Whether the window is resizable or not.
|
||||
/// When resizable is set to false, native window's maximize button is automatically disabled.
|
||||
#[must_use]
|
||||
fn resizable(self, resizable: bool) -> Self;
|
||||
|
||||
/// Whether the window's native maximize button is enabled or not.
|
||||
/// If resizable is set to false, this setting is ignored.
|
||||
///
|
||||
/// ## Platform-specific
|
||||
///
|
||||
/// - **macOS:** Disables the "zoom" button in the window titlebar, which is also used to enter fullscreen mode.
|
||||
/// - **Linux / iOS / Android:** Unsupported.
|
||||
#[must_use]
|
||||
fn maximizable(self, maximizable: bool) -> Self;
|
||||
|
||||
/// Whether the window's native minimize button is enabled or not.
|
||||
///
|
||||
/// ## Platform-specific
|
||||
///
|
||||
/// - **Linux / iOS / Android:** Unsupported.
|
||||
#[must_use]
|
||||
fn minimizable(self, minimizable: bool) -> Self;
|
||||
|
||||
/// Whether the window's native close button is enabled or not.
|
||||
///
|
||||
/// ## Platform-specific
|
||||
///
|
||||
/// - **Linux:** "GTK+ will do its best to convince the window manager not to show a close button.
|
||||
/// Depending on the system, this function may not have any effect when called on a window that is already visible"
|
||||
/// - **iOS / Android:** Unsupported.
|
||||
#[must_use]
|
||||
fn closable(self, closable: bool) -> Self;
|
||||
|
||||
/// The title of the window in the title bar.
|
||||
#[must_use]
|
||||
fn title<S: Into<String>>(self, title: S) -> Self;
|
||||
|
||||
/// Whether to start the window in fullscreen or not.
|
||||
#[must_use]
|
||||
fn fullscreen(self, fullscreen: bool) -> Self;
|
||||
|
||||
/// Whether the window will be initially focused or not.
|
||||
#[must_use]
|
||||
fn focused(self, focused: bool) -> Self;
|
||||
|
||||
/// Whether the window will be focusable or not.
|
||||
#[must_use]
|
||||
fn focusable(self, focusable: bool) -> Self;
|
||||
|
||||
/// Whether the window should be maximized upon creation.
|
||||
#[must_use]
|
||||
fn maximized(self, maximized: bool) -> Self;
|
||||
|
||||
/// Whether the window should be immediately visible upon creation.
|
||||
#[must_use]
|
||||
fn visible(self, visible: bool) -> Self;
|
||||
|
||||
/// Whether the window should be transparent. If this is true, writing colors
|
||||
/// with alpha values different than `1.0` will produce a transparent window.
|
||||
#[cfg(any(not(target_os = "macos"), feature = "macos-private-api"))]
|
||||
#[cfg_attr(
|
||||
docsrs,
|
||||
doc(cfg(any(not(target_os = "macos"), feature = "macos-private-api")))
|
||||
)]
|
||||
#[must_use]
|
||||
fn transparent(self, transparent: bool) -> Self;
|
||||
|
||||
/// Whether the window should have borders and bars.
|
||||
#[must_use]
|
||||
fn decorations(self, decorations: bool) -> Self;
|
||||
|
||||
/// Whether the window should always be below other windows.
|
||||
#[must_use]
|
||||
fn always_on_bottom(self, always_on_bottom: bool) -> Self;
|
||||
|
||||
/// Whether the window should always be on top of other windows.
|
||||
#[must_use]
|
||||
fn always_on_top(self, always_on_top: bool) -> Self;
|
||||
|
||||
/// Whether the window should be visible on all workspaces or virtual desktops.
|
||||
#[must_use]
|
||||
fn visible_on_all_workspaces(self, visible_on_all_workspaces: bool) -> Self;
|
||||
|
||||
/// Prevents the window contents from being captured by other apps.
|
||||
#[must_use]
|
||||
fn content_protected(self, protected: bool) -> Self;
|
||||
|
||||
/// Sets the window icon.
|
||||
fn icon(self, icon: Icon) -> crate::Result<Self>;
|
||||
|
||||
/// Sets whether or not the window icon should be added to the taskbar.
|
||||
#[must_use]
|
||||
fn skip_taskbar(self, skip: bool) -> Self;
|
||||
|
||||
/// Set the window background color.
|
||||
#[must_use]
|
||||
fn background_color(self, color: Color) -> Self;
|
||||
|
||||
/// Sets whether or not the window has shadow.
|
||||
///
|
||||
/// ## Platform-specific
|
||||
///
|
||||
/// - **Windows:**
|
||||
/// - `false` has no effect on decorated window, shadows are always ON.
|
||||
/// - `true` will make undecorated window have a 1px white border,
|
||||
/// and on Windows 11, it will have a rounded corners.
|
||||
/// - **Linux:** Unsupported.
|
||||
#[must_use]
|
||||
fn shadow(self, enable: bool) -> Self;
|
||||
|
||||
/// Set an owner to the window to be created.
|
||||
///
|
||||
/// From MSDN:
|
||||
/// - An owned window is always above its owner in the z-order.
|
||||
/// - The system automatically destroys an owned window when its owner is destroyed.
|
||||
/// - An owned window is hidden when its owner is minimized.
|
||||
///
|
||||
/// For more information, see <https://docs.microsoft.com/en-us/windows/win32/winmsg/window-features#owned-windows>
|
||||
#[cfg(windows)]
|
||||
#[must_use]
|
||||
fn owner(self, owner: HWND) -> Self;
|
||||
|
||||
/// Sets a parent to the window to be created.
|
||||
///
|
||||
/// A child window has the WS_CHILD style and is confined to the client area of its parent window.
|
||||
///
|
||||
/// For more information, see <https://docs.microsoft.com/en-us/windows/win32/winmsg/window-features#child-windows>
|
||||
#[cfg(windows)]
|
||||
#[must_use]
|
||||
fn parent(self, parent: HWND) -> Self;
|
||||
|
||||
/// Sets a parent to the window to be created.
|
||||
///
|
||||
/// See <https://developer.apple.com/documentation/appkit/nswindow/1419152-addchildwindow?language=objc>
|
||||
#[cfg(target_os = "macos")]
|
||||
#[must_use]
|
||||
fn parent(self, parent: *mut std::ffi::c_void) -> Self;
|
||||
|
||||
/// Sets the window to be created transient for parent.
|
||||
///
|
||||
/// See <https://docs.gtk.org/gtk3/method.Window.set_transient_for.html>
|
||||
#[cfg(any(
|
||||
target_os = "linux",
|
||||
target_os = "dragonfly",
|
||||
target_os = "freebsd",
|
||||
target_os = "netbsd",
|
||||
target_os = "openbsd"
|
||||
))]
|
||||
fn transient_for(self, parent: &impl gtk::glib::IsA<gtk::Window>) -> Self;
|
||||
|
||||
/// Enables or disables drag and drop support.
|
||||
#[cfg(windows)]
|
||||
#[must_use]
|
||||
fn drag_and_drop(self, enabled: bool) -> Self;
|
||||
|
||||
/// Hide the titlebar. Titlebar buttons will still be visible.
|
||||
#[cfg(target_os = "macos")]
|
||||
#[must_use]
|
||||
fn title_bar_style(self, style: tauri_utils::TitleBarStyle) -> Self;
|
||||
|
||||
/// Change the position of the window controls on macOS.
|
||||
///
|
||||
/// Requires titleBarStyle: Overlay and decorations: true.
|
||||
#[cfg(target_os = "macos")]
|
||||
#[must_use]
|
||||
fn traffic_light_position<P: Into<dpi::Position>>(self, position: P) -> Self;
|
||||
|
||||
/// Hide the window title.
|
||||
#[cfg(target_os = "macos")]
|
||||
#[must_use]
|
||||
fn hidden_title(self, hidden: bool) -> Self;
|
||||
|
||||
/// Defines the window [tabbing identifier] for macOS.
|
||||
///
|
||||
/// Windows with matching tabbing identifiers will be grouped together.
|
||||
/// If the tabbing identifier is not set, automatic tabbing will be disabled.
|
||||
///
|
||||
/// [tabbing identifier]: <https://developer.apple.com/documentation/appkit/nswindow/1644704-tabbingidentifier>
|
||||
#[cfg(target_os = "macos")]
|
||||
#[must_use]
|
||||
fn tabbing_identifier(self, identifier: &str) -> Self;
|
||||
|
||||
/// Forces a theme or uses the system settings if None was provided.
|
||||
fn theme(self, theme: Option<Theme>) -> Self;
|
||||
|
||||
/// Whether the icon was set or not.
|
||||
fn has_icon(&self) -> bool;
|
||||
|
||||
fn get_theme(&self) -> Option<Theme>;
|
||||
|
||||
/// Sets custom name for Windows' window class. **Windows only**.
|
||||
#[must_use]
|
||||
fn window_classname<S: Into<String>>(self, window_classname: S) -> Self;
|
||||
|
||||
/// The name of the activity to create for this webview window.
|
||||
#[cfg(target_os = "android")]
|
||||
fn activity_name<S: Into<String>>(self, class_name: S) -> Self;
|
||||
|
||||
/// Sets the name of the activity that is creating this webview window.
|
||||
///
|
||||
/// This is important to determine which stack the activity will belong to.
|
||||
#[cfg(target_os = "android")]
|
||||
fn created_by_activity_name<S: Into<String>>(self, class_name: S) -> Self;
|
||||
|
||||
/// Sets the identifier of the UIScene that is requesting the creation of this new scene,
|
||||
/// establishing a relationship between the two scenes.
|
||||
///
|
||||
/// By default the system uses the foreground scene.
|
||||
#[cfg(target_os = "ios")]
|
||||
fn requested_by_scene_identifier<S: Into<String>>(self, identifier: S) -> Self;
|
||||
}
|
||||
|
||||
/// A window that has yet to be built.
|
||||
pub struct PendingWindow<T: UserEvent, R: Runtime<T>> {
|
||||
/// The label that the window will be named.
|
||||
pub label: String,
|
||||
|
||||
/// The [`WindowBuilder`] that the window will be created with.
|
||||
pub window_builder: <R::WindowDispatcher as WindowDispatch<T>>::WindowBuilder,
|
||||
|
||||
/// The webview that gets added to the window. Optional in case you want to use child webviews or other window content instead.
|
||||
pub webview: Option<PendingWebview<T, R>>,
|
||||
}
|
||||
|
||||
pub fn is_label_valid(label: &str) -> bool {
|
||||
label
|
||||
.chars()
|
||||
.all(|c| char::is_alphanumeric(c) || c == '-' || c == '/' || c == ':' || c == '_')
|
||||
}
|
||||
|
||||
pub fn assert_label_is_valid(label: &str) {
|
||||
assert!(
|
||||
is_label_valid(label),
|
||||
"Window label must include only alphanumeric characters, `-`, `/`, `:` and `_`."
|
||||
);
|
||||
}
|
||||
|
||||
impl<T: UserEvent, R: Runtime<T>> PendingWindow<T, R> {
|
||||
/// Create a new [`PendingWindow`] with a label from the given [`WindowBuilder`].
|
||||
pub fn new(
|
||||
window_builder: <R::WindowDispatcher as WindowDispatch<T>>::WindowBuilder,
|
||||
label: impl Into<String>,
|
||||
) -> crate::Result<Self> {
|
||||
let label = label.into();
|
||||
if !is_label_valid(&label) {
|
||||
Err(crate::Error::InvalidWindowLabel)
|
||||
} else {
|
||||
Ok(Self {
|
||||
window_builder,
|
||||
label,
|
||||
webview: None,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets a webview to be created on the window.
|
||||
pub fn set_webview(&mut self, webview: PendingWebview<T, R>) -> &mut Self {
|
||||
self.webview.replace(webview);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
/// Identifier of a window.
|
||||
#[derive(Debug, Clone, Copy, Hash, Eq, PartialEq, Ord, PartialOrd)]
|
||||
pub struct WindowId(u32);
|
||||
|
||||
impl From<u32> for WindowId {
|
||||
fn from(value: u32) -> Self {
|
||||
Self(value)
|
||||
}
|
||||
}
|
||||
|
||||
/// A window that is not yet managed by Tauri.
|
||||
#[derive(Debug)]
|
||||
pub struct DetachedWindow<T: UserEvent, R: Runtime<T>> {
|
||||
/// The identifier of the window.
|
||||
pub id: WindowId,
|
||||
/// Name of the window
|
||||
pub label: String,
|
||||
|
||||
/// The [`WindowDispatch`] associated with the window.
|
||||
pub dispatcher: R::WindowDispatcher,
|
||||
|
||||
/// The webview dispatcher in case this window has an attached webview.
|
||||
pub webview: Option<DetachedWindowWebview<T, R>>,
|
||||
}
|
||||
|
||||
/// A detached webview associated with a window.
|
||||
#[derive(Debug)]
|
||||
pub struct DetachedWindowWebview<T: UserEvent, R: Runtime<T>> {
|
||||
pub webview: DetachedWebview<T, R>,
|
||||
pub use_https_scheme: bool,
|
||||
/// Whether devtools was enabled in [`crate::webview::WebviewAttributes`]. `Some(false)` disables the inspector.
|
||||
pub devtools: Option<bool>,
|
||||
}
|
||||
|
||||
impl<T: UserEvent, R: Runtime<T>> Clone for DetachedWindowWebview<T, R> {
|
||||
fn clone(&self) -> Self {
|
||||
Self {
|
||||
webview: self.webview.clone(),
|
||||
use_https_scheme: self.use_https_scheme,
|
||||
devtools: self.devtools,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: UserEvent, R: Runtime<T>> Clone for DetachedWindow<T, R> {
|
||||
fn clone(&self) -> Self {
|
||||
Self {
|
||||
id: self.id,
|
||||
label: self.label.clone(),
|
||||
dispatcher: self.dispatcher.clone(),
|
||||
webview: self.webview.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: UserEvent, R: Runtime<T>> Hash for DetachedWindow<T, R> {
|
||||
/// Only use the [`DetachedWindow`]'s label to represent its hash.
|
||||
fn hash<H: Hasher>(&self, state: &mut H) {
|
||||
self.label.hash(state)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: UserEvent, R: Runtime<T>> Eq for DetachedWindow<T, R> {}
|
||||
impl<T: UserEvent, R: Runtime<T>> PartialEq for DetachedWindow<T, R> {
|
||||
/// Only use the [`DetachedWindow`]'s label to compare equality.
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.label.eq(&other.label)
|
||||
}
|
||||
}
|
||||
|
||||
/// A raw window type that contains fields to access
|
||||
/// the HWND on Windows, gtk::ApplicationWindow on Linux
|
||||
pub struct RawWindow<'a> {
|
||||
#[cfg(windows)]
|
||||
pub hwnd: isize,
|
||||
#[cfg(any(
|
||||
target_os = "linux",
|
||||
target_os = "dragonfly",
|
||||
target_os = "freebsd",
|
||||
target_os = "netbsd",
|
||||
target_os = "openbsd"
|
||||
))]
|
||||
pub gtk_window: &'a gtk::ApplicationWindow,
|
||||
#[cfg(any(
|
||||
target_os = "linux",
|
||||
target_os = "dragonfly",
|
||||
target_os = "freebsd",
|
||||
target_os = "netbsd",
|
||||
target_os = "openbsd"
|
||||
))]
|
||||
pub default_vbox: Option<&'a gtk::Box>,
|
||||
pub _marker: &'a PhantomData<()>,
|
||||
}
|
||||
1035
apps/tauri/src-tauri/vendor/tauri-utils/CHANGELOG.md
vendored
Normal file
1035
apps/tauri/src-tauri/vendor/tauri-utils/CHANGELOG.md
vendored
Normal file
File diff suppressed because it is too large
Load diff
94
apps/tauri/src-tauri/vendor/tauri-utils/Cargo.toml
vendored
Normal file
94
apps/tauri/src-tauri/vendor/tauri-utils/Cargo.toml
vendored
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
[package]
|
||||
name = "tauri-utils"
|
||||
version = "2.9.0"
|
||||
description = "Utilities for Tauri"
|
||||
exclude = ["CHANGELOG.md", "/target"]
|
||||
readme = "README.md"
|
||||
authors.workspace = true
|
||||
homepage.workspace = true
|
||||
repository.workspace = true
|
||||
categories.workspace = true
|
||||
license.workspace = true
|
||||
edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
|
||||
[dependencies]
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
anyhow = "1"
|
||||
thiserror = "2"
|
||||
phf = { version = "0.11", features = ["macros"] }
|
||||
brotli = { version = "8", optional = true, default-features = false, features = [
|
||||
"std",
|
||||
] }
|
||||
url = { version = "2", features = ["serde"] }
|
||||
html5ever = { version = "0.29", optional = true }
|
||||
kuchiki = { package = "kuchikiki", version = "0.8.8-speedreader", optional = true }
|
||||
dom_query = { version = "0.27", optional = true, default-features = false }
|
||||
proc-macro2 = { version = "1", optional = true }
|
||||
quote = { version = "1", optional = true }
|
||||
schemars = { version = "1", features = ["url2", "uuid1"], optional = true }
|
||||
serde_with = "3"
|
||||
aes-gcm = { version = "0.10", optional = true }
|
||||
getrandom = { version = "0.3", optional = true, features = ["std"] }
|
||||
serialize-to-javascript = { version = "0.1.2", optional = true }
|
||||
ctor = { version = "0.8", default-features = false, features = [
|
||||
"std",
|
||||
"proc_macro",
|
||||
] }
|
||||
json5 = { version = "0.4", optional = true }
|
||||
# Part of public api in error type
|
||||
toml = { version = ">=0.9, <=1", features = ["parse"] }
|
||||
json-patch = "3.0"
|
||||
# Our code requires at least 0.3.1
|
||||
glob = "0.3.1"
|
||||
urlpattern = "0.3"
|
||||
regex = "1"
|
||||
walkdir = { version = "2", optional = true }
|
||||
memchr = "2"
|
||||
semver = "1"
|
||||
infer = "0.19"
|
||||
dunce = "1"
|
||||
log = "0.4.21"
|
||||
cargo_metadata = { version = "0.19", optional = true }
|
||||
serde-untagged = "0.1"
|
||||
uuid = { version = "1", features = ["serde"] }
|
||||
http = "1"
|
||||
plist = "1"
|
||||
|
||||
[target."cfg(target_os = \"macos\")".dependencies]
|
||||
swift-rs = { version = "1", optional = true, features = ["build"] }
|
||||
|
||||
[dev-dependencies]
|
||||
getrandom = { version = "0.3", features = ["std"] }
|
||||
serial_test = "3"
|
||||
tauri = { path = "../tauri" }
|
||||
tempfile = "3.15.0"
|
||||
|
||||
[features]
|
||||
build = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"cargo_metadata",
|
||||
"schema",
|
||||
"swift-rs",
|
||||
"html-manipulation",
|
||||
]
|
||||
# Same as `build` but uses `html-manipulation-2` to avoid the `kuchikiki` dependency.
|
||||
build-2 = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"cargo_metadata",
|
||||
"schema",
|
||||
"swift-rs",
|
||||
"html-manipulation-2",
|
||||
]
|
||||
compression = ["brotli"]
|
||||
schema = ["schemars"]
|
||||
isolation = ["aes-gcm", "getrandom", "serialize-to-javascript"]
|
||||
process-relaunch-dangerous-allow-symlink-macos = []
|
||||
config-json5 = ["json5"]
|
||||
config-toml = []
|
||||
resources = ["walkdir"]
|
||||
html-manipulation = ["dep:html5ever", "dep:kuchiki"]
|
||||
html-manipulation-2 = ["dep:dom_query"]
|
||||
177
apps/tauri/src-tauri/vendor/tauri-utils/LICENSE_APACHE-2.0
vendored
Normal file
177
apps/tauri/src-tauri/vendor/tauri-utils/LICENSE_APACHE-2.0
vendored
Normal file
|
|
@ -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
|
||||
21
apps/tauri/src-tauri/vendor/tauri-utils/LICENSE_MIT
vendored
Normal file
21
apps/tauri/src-tauri/vendor/tauri-utils/LICENSE_MIT
vendored
Normal file
|
|
@ -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.
|
||||
42
apps/tauri/src-tauri/vendor/tauri-utils/README.md
vendored
Normal file
42
apps/tauri/src-tauri/vendor/tauri-utils/README.md
vendored
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
# tauri-utils
|
||||
|
||||
<img align="right" src="https://github.com/tauri-apps/tauri/raw/dev/.github/icon.png" height="128" width="128">
|
||||
|
||||
[](https://github.com/tauri-apps/tauri/tree/dev)
|
||||
[](https://opencollective.com/tauri)
|
||||
[](https://github.com/tauri-apps/tauri/actions/workflows/test-core.yml)
|
||||
[](https://app.fossa.com/projects/git%2Bgithub.com%2Ftauri-apps%2Ftauri?ref=badge_shield)
|
||||
[](https://discord.gg/SpmNs4S)
|
||||
[](https://tauri.app)
|
||||
[](https://good-labs.github.io/greater-good-affirmation)
|
||||
[](https://opencollective.com/tauri)
|
||||
|
||||
| Component | Version |
|
||||
| ----------- | ---------------------------------------------------------------------------------------------------------- |
|
||||
| tauri-utils | [](https://crates.io/crates/tauri-utils) |
|
||||
|
||||
## About Tauri
|
||||
|
||||
Tauri is a polyglot and generic system that is very composable and allows engineers to make a wide variety of applications. It is used for building applications for Desktop Computers using a combination of Rust tools and HTML rendered in a Webview. Apps built with Tauri can ship with any number of pieces of an optional JS API / Rust API so that webviews can control the system via message passing. In fact, developers can extend the default API with their own functionality and bridge the Webview and Rust-based backend easily.
|
||||
|
||||
Tauri apps can have custom menus and have tray-type interfaces. They can be updated, and are managed by the user's operating system as expected. They are very small, because they use the system's webview. They do not ship a runtime, since the final binary is compiled from rust. This makes the reversing of Tauri apps not a trivial task.
|
||||
|
||||
## This module
|
||||
|
||||
This is common code that is reused in many places and offers useful utilities like parsing configuration files, detecting platform triples, injecting the CSP, and managing assets.
|
||||
|
||||
To learn more about the details of how all of these pieces fit together, please consult this [ARCHITECTURE.md](https://github.com/tauri-apps/tauri/blob/dev/ARCHITECTURE.md) document.
|
||||
|
||||
## Semver
|
||||
|
||||
**tauri** is following [Semantic Versioning 2.0](https://semver.org/).
|
||||
|
||||
## Licenses
|
||||
|
||||
Code: (c) 2021 - The Tauri Programme within The Commons Conservancy.
|
||||
|
||||
MIT or MIT/Apache 2.0 where applicable.
|
||||
|
||||
Logo: CC-BY-NC-ND
|
||||
|
||||
- Original Tauri Logo Designs by [Daniel Thompson-Yvetot](https://github.com/nothingismagick) and [Guillaume Chau](https://github.com/akryum)
|
||||
500
apps/tauri/src-tauri/vendor/tauri-utils/src/acl/build.rs
vendored
Normal file
500
apps/tauri/src-tauri/vendor/tauri-utils/src/acl/build.rs
vendored
Normal file
|
|
@ -0,0 +1,500 @@
|
|||
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
//! ACL items that are only useful inside of build script/codegen context.
|
||||
|
||||
use std::{
|
||||
collections::{BTreeMap, HashMap},
|
||||
env, fs,
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
use crate::{
|
||||
acl::{AllowedCommands, Error, has_app_manifest},
|
||||
config::Config,
|
||||
write_if_changed,
|
||||
};
|
||||
|
||||
use super::{
|
||||
ALLOWED_COMMANDS_FILE_NAME, PERMISSION_SCHEMA_FILE_NAME, PERMISSION_SCHEMAS_FOLDER_NAME,
|
||||
REMOVE_UNUSED_COMMANDS_ENV_VAR,
|
||||
capability::{Capability, CapabilityFile},
|
||||
manifest::PermissionFile,
|
||||
};
|
||||
|
||||
/// Known name of the folder containing autogenerated permissions.
|
||||
pub const AUTOGENERATED_FOLDER_NAME: &str = "autogenerated";
|
||||
|
||||
/// Cargo cfg key for permissions file paths
|
||||
pub const PERMISSION_FILES_PATH_KEY: &str = "PERMISSION_FILES_PATH";
|
||||
|
||||
/// Cargo cfg key for global scope schemas
|
||||
pub const GLOBAL_SCOPE_SCHEMA_PATH_KEY: &str = "GLOBAL_SCOPE_SCHEMA_PATH";
|
||||
|
||||
/// Allowed permission file extensions
|
||||
pub const PERMISSION_FILE_EXTENSIONS: &[&str] = &["json", "toml"];
|
||||
|
||||
/// Known filename of the permission documentation file
|
||||
pub const PERMISSION_DOCS_FILE_NAME: &str = "reference.md";
|
||||
|
||||
/// Allowed capability file extensions
|
||||
const CAPABILITY_FILE_EXTENSIONS: &[&str] = &[
|
||||
"json",
|
||||
#[cfg(feature = "config-json5")]
|
||||
"json5",
|
||||
"toml",
|
||||
];
|
||||
|
||||
/// Known folder name of the capability schemas
|
||||
const CAPABILITIES_SCHEMA_FOLDER_NAME: &str = "schemas";
|
||||
|
||||
const CORE_PLUGIN_PERMISSIONS_TOKEN: &str = "__CORE_PLUGIN__";
|
||||
|
||||
fn parse_permissions(paths: Vec<PathBuf>) -> Result<Vec<PermissionFile>, Error> {
|
||||
let mut permissions = Vec::new();
|
||||
for path in paths {
|
||||
let ext = path.extension().unwrap().to_string_lossy().to_string();
|
||||
let permission_file = fs::read_to_string(&path).map_err(|e| Error::ReadFile(e, path))?;
|
||||
let permission: PermissionFile = match ext.as_str() {
|
||||
"toml" => toml::from_str(&permission_file)?,
|
||||
"json" => serde_json::from_str(&permission_file)?,
|
||||
_ => return Err(Error::UnknownPermissionFormat(ext)),
|
||||
};
|
||||
permissions.push(permission);
|
||||
}
|
||||
Ok(permissions)
|
||||
}
|
||||
|
||||
/// Write the permissions to a temporary directory and pass it to the immediate consuming crate.
|
||||
pub fn define_permissions<F: Fn(&Path) -> bool>(
|
||||
pattern: &str,
|
||||
pkg_name: &str,
|
||||
out_dir: &Path,
|
||||
filter_fn: F,
|
||||
) -> Result<Vec<PermissionFile>, Error> {
|
||||
let permission_files = glob::glob(pattern)?
|
||||
.flatten()
|
||||
.flat_map(|p| p.canonicalize())
|
||||
// filter extension
|
||||
.filter(|p| {
|
||||
p.extension()
|
||||
.and_then(|e| e.to_str())
|
||||
.map(|e| PERMISSION_FILE_EXTENSIONS.contains(&e))
|
||||
.unwrap_or_default()
|
||||
})
|
||||
.filter(|p| filter_fn(p))
|
||||
// filter schemas
|
||||
.filter(|p| p.parent().unwrap().file_name().unwrap() != PERMISSION_SCHEMAS_FOLDER_NAME)
|
||||
.collect::<Vec<PathBuf>>();
|
||||
|
||||
let pkg_name_valid_path = pkg_name.replace(':', "-");
|
||||
let permission_files_path = out_dir.join(format!("{pkg_name_valid_path}-permission-files"));
|
||||
let permission_files_json = serde_json::to_string(&permission_files)?;
|
||||
|
||||
write_if_changed(&permission_files_path, permission_files_json)
|
||||
.map_err(|e| Error::WriteFile(e, permission_files_path.clone()))?;
|
||||
|
||||
if let Some(plugin_name) = pkg_name.strip_prefix("tauri:") {
|
||||
println!(
|
||||
"cargo:{plugin_name}{CORE_PLUGIN_PERMISSIONS_TOKEN}_{PERMISSION_FILES_PATH_KEY}={}",
|
||||
permission_files_path.display()
|
||||
);
|
||||
} else {
|
||||
println!(
|
||||
"cargo:{PERMISSION_FILES_PATH_KEY}={}",
|
||||
permission_files_path.display()
|
||||
);
|
||||
}
|
||||
|
||||
parse_permissions(permission_files)
|
||||
}
|
||||
|
||||
/// Read all permissions listed from the defined cargo cfg key value.
|
||||
pub fn read_permissions() -> Result<HashMap<String, Vec<PermissionFile>>, Error> {
|
||||
let mut permissions_map = HashMap::new();
|
||||
|
||||
for (key, value) in env::vars_os() {
|
||||
let key = key.to_string_lossy();
|
||||
|
||||
if let Some(plugin_crate_name_var) = key
|
||||
.strip_prefix("DEP_")
|
||||
.and_then(|v| v.strip_suffix(&format!("_{PERMISSION_FILES_PATH_KEY}")))
|
||||
.map(|v| {
|
||||
v.strip_suffix(CORE_PLUGIN_PERMISSIONS_TOKEN)
|
||||
.and_then(|v| v.strip_prefix("TAURI_"))
|
||||
.unwrap_or(v)
|
||||
})
|
||||
{
|
||||
let permissions_path = PathBuf::from(value);
|
||||
let permissions_str =
|
||||
fs::read_to_string(&permissions_path).map_err(|e| Error::ReadFile(e, permissions_path))?;
|
||||
let permissions: Vec<PathBuf> = serde_json::from_str(&permissions_str)?;
|
||||
let permissions = parse_permissions(permissions)?;
|
||||
|
||||
let plugin_crate_name = plugin_crate_name_var.to_lowercase().replace('_', "-");
|
||||
let plugin_crate_name = plugin_crate_name
|
||||
.strip_prefix("tauri-plugin-")
|
||||
.map(ToString::to_string)
|
||||
.unwrap_or(plugin_crate_name);
|
||||
|
||||
permissions_map.insert(plugin_crate_name, permissions);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(permissions_map)
|
||||
}
|
||||
|
||||
/// Define the global scope schema JSON file path if it exists and pass it to the immediate consuming crate.
|
||||
pub fn define_global_scope_schema(
|
||||
schema: schemars::Schema,
|
||||
pkg_name: &str,
|
||||
out_dir: &Path,
|
||||
) -> Result<(), Error> {
|
||||
let path = out_dir.join("global-scope.json");
|
||||
write_if_changed(&path, serde_json::to_vec(&schema)?)
|
||||
.map_err(|e| Error::WriteFile(e, path.clone()))?;
|
||||
|
||||
if let Some(plugin_name) = pkg_name.strip_prefix("tauri:") {
|
||||
println!(
|
||||
"cargo:{plugin_name}{CORE_PLUGIN_PERMISSIONS_TOKEN}_{GLOBAL_SCOPE_SCHEMA_PATH_KEY}={}",
|
||||
path.display()
|
||||
);
|
||||
} else {
|
||||
println!("cargo:{GLOBAL_SCOPE_SCHEMA_PATH_KEY}={}", path.display());
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Read all global scope schemas listed from the defined cargo cfg key value.
|
||||
pub fn read_global_scope_schemas() -> Result<HashMap<String, serde_json::Value>, Error> {
|
||||
let mut schemas_map = HashMap::new();
|
||||
|
||||
for (key, value) in env::vars_os() {
|
||||
let key = key.to_string_lossy();
|
||||
|
||||
if let Some(plugin_crate_name_var) = key
|
||||
.strip_prefix("DEP_")
|
||||
.and_then(|v| v.strip_suffix(&format!("_{GLOBAL_SCOPE_SCHEMA_PATH_KEY}")))
|
||||
.map(|v| {
|
||||
v.strip_suffix(CORE_PLUGIN_PERMISSIONS_TOKEN)
|
||||
.and_then(|v| v.strip_prefix("TAURI_"))
|
||||
.unwrap_or(v)
|
||||
})
|
||||
{
|
||||
let path = PathBuf::from(value);
|
||||
let json = fs::read_to_string(&path).map_err(|e| Error::ReadFile(e, path))?;
|
||||
let schema: serde_json::Value = serde_json::from_str(&json)?;
|
||||
|
||||
let plugin_crate_name = plugin_crate_name_var.to_lowercase().replace('_', "-");
|
||||
let plugin_crate_name = plugin_crate_name
|
||||
.strip_prefix("tauri-plugin-")
|
||||
.map(ToString::to_string)
|
||||
.unwrap_or(plugin_crate_name);
|
||||
|
||||
schemas_map.insert(plugin_crate_name, schema);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(schemas_map)
|
||||
}
|
||||
|
||||
/// Parses all capability files with the given glob pattern.
|
||||
pub fn parse_capabilities(pattern: &str) -> Result<BTreeMap<String, Capability>, Error> {
|
||||
let mut capabilities_map = BTreeMap::new();
|
||||
|
||||
for path in glob::glob(pattern)?
|
||||
.flatten() // filter extension
|
||||
.filter(|p| {
|
||||
p.extension()
|
||||
.and_then(|e| e.to_str())
|
||||
.map(|e| CAPABILITY_FILE_EXTENSIONS.contains(&e))
|
||||
.unwrap_or_default()
|
||||
})
|
||||
// filter schema files
|
||||
// TODO: remove this before stable
|
||||
.filter(|p| p.parent().unwrap().file_name().unwrap() != CAPABILITIES_SCHEMA_FOLDER_NAME)
|
||||
{
|
||||
match CapabilityFile::load(&path)? {
|
||||
CapabilityFile::Capability(capability) => {
|
||||
if capabilities_map.contains_key(&capability.identifier) {
|
||||
return Err(Error::CapabilityAlreadyExists {
|
||||
identifier: capability.identifier,
|
||||
});
|
||||
}
|
||||
|
||||
capabilities_map.insert(capability.identifier.clone(), capability);
|
||||
}
|
||||
CapabilityFile::List(capabilities) | CapabilityFile::NamedList { capabilities } => {
|
||||
for capability in capabilities {
|
||||
if capabilities_map.contains_key(&capability.identifier) {
|
||||
return Err(Error::CapabilityAlreadyExists {
|
||||
identifier: capability.identifier,
|
||||
});
|
||||
}
|
||||
|
||||
capabilities_map.insert(capability.identifier.clone(), capability);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(capabilities_map)
|
||||
}
|
||||
|
||||
/// Permissions that are generated from commands using [`autogenerate_command_permissions`].
|
||||
pub struct AutogeneratedPermissions {
|
||||
/// The allow permissions generated from commands.
|
||||
pub allowed: Vec<String>,
|
||||
/// The deny permissions generated from commands.
|
||||
pub denied: Vec<String>,
|
||||
}
|
||||
|
||||
/// Autogenerate permission files for a list of commands.
|
||||
pub fn autogenerate_command_permissions(
|
||||
path: &Path,
|
||||
commands: &[&str],
|
||||
license_header: &str,
|
||||
schema_ref: bool,
|
||||
) -> AutogeneratedPermissions {
|
||||
if !path.exists() {
|
||||
fs::create_dir_all(path).expect("unable to create autogenerated commands dir");
|
||||
}
|
||||
|
||||
let schema_entry = if schema_ref {
|
||||
let cwd = env::current_dir().unwrap();
|
||||
let components_len = path.strip_prefix(&cwd).unwrap_or(path).components().count();
|
||||
let schema_path = (1..components_len)
|
||||
.map(|_| "..")
|
||||
.collect::<PathBuf>()
|
||||
.join(PERMISSION_SCHEMAS_FOLDER_NAME)
|
||||
.join(PERMISSION_SCHEMA_FILE_NAME);
|
||||
format!(
|
||||
"\n\"$schema\" = \"{}\"\n",
|
||||
dunce::simplified(&schema_path)
|
||||
.display()
|
||||
.to_string()
|
||||
.replace('\\', "/")
|
||||
)
|
||||
} else {
|
||||
"".to_string()
|
||||
};
|
||||
|
||||
let mut autogenerated = AutogeneratedPermissions {
|
||||
allowed: Vec::new(),
|
||||
denied: Vec::new(),
|
||||
};
|
||||
|
||||
for command in commands {
|
||||
let slugified_command = command.replace('_', "-");
|
||||
|
||||
let toml = format!(
|
||||
r###"{license_header}# Automatically generated - DO NOT EDIT!
|
||||
{schema_entry}
|
||||
[[permission]]
|
||||
identifier = "allow-{slugified_command}"
|
||||
description = "Enables the {command} command without any pre-configured scope."
|
||||
commands.allow = ["{command}"]
|
||||
|
||||
[[permission]]
|
||||
identifier = "deny-{slugified_command}"
|
||||
description = "Denies the {command} command without any pre-configured scope."
|
||||
commands.deny = ["{command}"]
|
||||
"###,
|
||||
);
|
||||
|
||||
let out_path = path.join(format!("{command}.toml"));
|
||||
write_if_changed(&out_path, toml)
|
||||
.unwrap_or_else(|_| panic!("unable to autogenerate {out_path:?}"));
|
||||
|
||||
autogenerated
|
||||
.allowed
|
||||
.push(format!("allow-{slugified_command}"));
|
||||
autogenerated
|
||||
.denied
|
||||
.push(format!("deny-{slugified_command}"));
|
||||
}
|
||||
|
||||
autogenerated
|
||||
}
|
||||
|
||||
const PERMISSION_TABLE_HEADER: &str =
|
||||
"## Permission Table\n\n<table>\n<tr>\n<th>Identifier</th>\n<th>Description</th>\n</tr>\n";
|
||||
|
||||
/// Generate a markdown documentation page containing the list of permissions of the plugin.
|
||||
pub fn generate_docs(
|
||||
permissions: &[PermissionFile],
|
||||
out_dir: &Path,
|
||||
plugin_identifier: &str,
|
||||
) -> Result<(), Error> {
|
||||
let mut default_permission = "".to_owned();
|
||||
let mut permission_table = "".to_string();
|
||||
|
||||
fn docs_from(id: &str, description: Option<&str>, plugin_identifier: &str) -> String {
|
||||
let mut docs = format!("\n<tr>\n<td>\n\n`{plugin_identifier}:{id}`\n\n</td>\n");
|
||||
if let Some(d) = description {
|
||||
docs.push_str(&format!("<td>\n\n{d}\n\n</td>"));
|
||||
}
|
||||
docs.push_str("\n</tr>");
|
||||
docs
|
||||
}
|
||||
|
||||
for permission in permissions {
|
||||
for set in &permission.set {
|
||||
permission_table.push_str(&docs_from(
|
||||
&set.identifier,
|
||||
Some(&set.description),
|
||||
plugin_identifier,
|
||||
));
|
||||
permission_table.push('\n');
|
||||
}
|
||||
|
||||
if let Some(default) = &permission.default {
|
||||
default_permission.push_str("## Default Permission\n\n");
|
||||
default_permission.push_str(default.description.as_deref().unwrap_or_default().trim());
|
||||
default_permission.push('\n');
|
||||
default_permission.push('\n');
|
||||
if !default.permissions.is_empty() {
|
||||
default_permission.push_str("#### This default permission set includes the following:\n\n");
|
||||
for permission in &default.permissions {
|
||||
default_permission.push_str(&format!("- `{permission}`\n"));
|
||||
}
|
||||
default_permission.push('\n');
|
||||
}
|
||||
}
|
||||
|
||||
for permission in &permission.permission {
|
||||
permission_table.push_str(&docs_from(
|
||||
&permission.identifier,
|
||||
permission.description.as_deref(),
|
||||
plugin_identifier,
|
||||
));
|
||||
permission_table.push('\n');
|
||||
}
|
||||
}
|
||||
|
||||
let docs = format!("{default_permission}{PERMISSION_TABLE_HEADER}\n{permission_table}</table>\n");
|
||||
|
||||
let reference_path = out_dir.join(PERMISSION_DOCS_FILE_NAME);
|
||||
write_if_changed(&reference_path, docs).map_err(|e| Error::WriteFile(e, reference_path))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// TODO: We have way too many duplicated code around getting the config files, e.g.
|
||||
// - crates/tauri-codegen/src/lib.rs (`get_config`)
|
||||
// - crates/tauri-build/src/lib.rs (`try_build`)
|
||||
// - crates/tauri-cli/src/helpers/config.rs (`get_internal`)
|
||||
/// Generate allowed commands file for the `generate_handler` macro to remove never allowed commands
|
||||
pub fn generate_allowed_commands(
|
||||
out_dir: &Path,
|
||||
capabilities_from_files: Option<BTreeMap<String, Capability>>,
|
||||
permissions_map: BTreeMap<String, Vec<PermissionFile>>,
|
||||
) -> Result<(), anyhow::Error> {
|
||||
println!("cargo:rerun-if-env-changed={REMOVE_UNUSED_COMMANDS_ENV_VAR}");
|
||||
|
||||
let allowed_commands_file_path = out_dir.join(ALLOWED_COMMANDS_FILE_NAME);
|
||||
|
||||
let remove_unused_commands_env_var = std::env::var(REMOVE_UNUSED_COMMANDS_ENV_VAR);
|
||||
|
||||
let should_generate_allowed_commands =
|
||||
remove_unused_commands_env_var.is_ok() && !permissions_map.is_empty();
|
||||
|
||||
if !should_generate_allowed_commands {
|
||||
let _ = std::fs::remove_file(allowed_commands_file_path);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// It's safe to `unwrap` here since we have checked if the result is ok above
|
||||
let config_directory = PathBuf::from(remove_unused_commands_env_var.unwrap());
|
||||
let capabilities_path = config_directory.join("capabilities");
|
||||
// Cargo re-builds if the variable points to an empty path,
|
||||
// so we check for exists here
|
||||
// see https://github.com/rust-lang/cargo/issues/4213
|
||||
if capabilities_path.exists() {
|
||||
println!("cargo:rerun-if-changed={}", capabilities_path.display());
|
||||
}
|
||||
|
||||
let target_triple = env::var("TARGET")?;
|
||||
let target = crate::platform::Target::from_triple(&target_triple);
|
||||
let (mut config, config_paths) = crate::config::parse::read_from(target, &config_directory)?;
|
||||
|
||||
for config_file_path in config_paths {
|
||||
println!("cargo:rerun-if-changed={}", config_file_path.display());
|
||||
}
|
||||
|
||||
if let Ok(env) = std::env::var("TAURI_CONFIG") {
|
||||
let merge_config: serde_json::Value = serde_json::from_str(&env)?;
|
||||
json_patch::merge(&mut config, &merge_config);
|
||||
}
|
||||
|
||||
println!("cargo:rerun-if-env-changed=TAURI_CONFIG");
|
||||
|
||||
// Set working directory to where `tauri.config.json` is, so that relative paths in it are parsed correctly.
|
||||
let old_cwd = std::env::current_dir()?;
|
||||
std::env::set_current_dir(config_directory)?;
|
||||
|
||||
let config: Config = serde_json::from_value(config)?;
|
||||
|
||||
// Reset working directory.
|
||||
std::env::set_current_dir(old_cwd)?;
|
||||
|
||||
let acl: BTreeMap<String, crate::acl::manifest::Manifest> = permissions_map
|
||||
.into_iter()
|
||||
.map(|(key, permissions)| {
|
||||
let key = key
|
||||
.strip_prefix("tauri-plugin-")
|
||||
.unwrap_or(&key)
|
||||
.to_string();
|
||||
let manifest = crate::acl::manifest::Manifest::new(permissions, None);
|
||||
(key, manifest)
|
||||
})
|
||||
.collect();
|
||||
|
||||
let capabilities_from_files = if let Some(capabilities) = capabilities_from_files {
|
||||
capabilities
|
||||
} else {
|
||||
crate::acl::build::parse_capabilities(&format!(
|
||||
"{}/**/*",
|
||||
glob::Pattern::escape(&capabilities_path.to_string_lossy())
|
||||
))?
|
||||
};
|
||||
let capabilities = crate::acl::get_capabilities(&config, capabilities_from_files, None)?;
|
||||
|
||||
let permission_entries = capabilities
|
||||
.into_values()
|
||||
.flat_map(|capabilities| capabilities.permissions);
|
||||
let mut allowed_commands = AllowedCommands {
|
||||
has_app_acl: has_app_manifest(&acl),
|
||||
..Default::default()
|
||||
};
|
||||
for permission_entry in permission_entries {
|
||||
let Ok(permissions) =
|
||||
crate::acl::resolved::get_permissions(permission_entry.identifier(), &acl)
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
for permission in permissions {
|
||||
let plugin_name = permission.key;
|
||||
let allowed_command_names = &permission.permission.commands.allow;
|
||||
for allowed_command in allowed_command_names {
|
||||
let command_name = if plugin_name == crate::acl::APP_ACL_KEY {
|
||||
allowed_command.to_string()
|
||||
} else if let Some(core_plugin_name) = plugin_name.strip_prefix("core:") {
|
||||
format!("plugin:{core_plugin_name}|{allowed_command}")
|
||||
} else {
|
||||
format!("plugin:{plugin_name}|{allowed_command}")
|
||||
};
|
||||
allowed_commands.commands.insert(command_name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
write_if_changed(
|
||||
allowed_commands_file_path,
|
||||
serde_json::to_string(&allowed_commands)?,
|
||||
)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
452
apps/tauri/src-tauri/vendor/tauri-utils/src/acl/capability.rs
vendored
Normal file
452
apps/tauri/src-tauri/vendor/tauri-utils/src/acl/capability.rs
vendored
Normal file
|
|
@ -0,0 +1,452 @@
|
|||
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
//! End-user abstraction for selecting permissions a window has access to.
|
||||
|
||||
use std::{path::Path, str::FromStr};
|
||||
|
||||
use crate::{acl::Identifier, platform::Target};
|
||||
use serde::{
|
||||
Deserialize, Deserializer, Serialize,
|
||||
de::{Error, IntoDeserializer},
|
||||
};
|
||||
use serde_untagged::UntaggedEnumVisitor;
|
||||
|
||||
use super::Scopes;
|
||||
|
||||
/// An entry for a permission value in a [`Capability`] can be either a raw permission [`Identifier`]
|
||||
/// or an object that references a permission and extends its scope.
|
||||
#[derive(Debug, Clone, PartialEq, Serialize)]
|
||||
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
|
||||
#[serde(untagged)]
|
||||
pub enum PermissionEntry {
|
||||
/// Reference a permission or permission set by identifier.
|
||||
PermissionRef(Identifier),
|
||||
/// Reference a permission or permission set by identifier and extends its scope.
|
||||
ExtendedPermission {
|
||||
/// Identifier of the permission or permission set.
|
||||
identifier: Identifier,
|
||||
/// Scope to append to the existing permission scope.
|
||||
#[serde(default, flatten)]
|
||||
scope: Scopes,
|
||||
},
|
||||
}
|
||||
|
||||
impl PermissionEntry {
|
||||
/// The identifier of the permission referenced in this entry.
|
||||
pub fn identifier(&self) -> &Identifier {
|
||||
match self {
|
||||
Self::PermissionRef(identifier) => identifier,
|
||||
Self::ExtendedPermission {
|
||||
identifier,
|
||||
scope: _,
|
||||
} => identifier,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for PermissionEntry {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
#[derive(Deserialize)]
|
||||
struct ExtendedPermissionStruct {
|
||||
identifier: Identifier,
|
||||
#[serde(default, flatten)]
|
||||
scope: Scopes,
|
||||
}
|
||||
|
||||
UntaggedEnumVisitor::new()
|
||||
.string(|string| {
|
||||
let de = string.into_deserializer();
|
||||
Identifier::deserialize(de).map(Self::PermissionRef)
|
||||
})
|
||||
.map(|map| {
|
||||
let ext_perm = map.deserialize::<ExtendedPermissionStruct>()?;
|
||||
Ok(Self::ExtendedPermission {
|
||||
identifier: ext_perm.identifier,
|
||||
scope: ext_perm.scope,
|
||||
})
|
||||
})
|
||||
.deserialize(deserializer)
|
||||
}
|
||||
}
|
||||
|
||||
/// A grouping and boundary mechanism developers can use to isolate access to the IPC layer.
|
||||
///
|
||||
/// It controls application windows' and webviews' fine grained access
|
||||
/// to the Tauri core, application, or plugin commands.
|
||||
/// If a webview or its window is not matching any capability then it has no access to the IPC layer at all.
|
||||
///
|
||||
/// This can be done to create groups of windows, based on their required system access, which can reduce
|
||||
/// impact of frontend vulnerabilities in less privileged windows.
|
||||
/// Windows can be added to a capability by exact name (e.g. `main-window`) or glob patterns like `*` or `admin-*`.
|
||||
/// A Window can have none, one, or multiple associated capabilities.
|
||||
///
|
||||
/// ## Example
|
||||
///
|
||||
/// ```json
|
||||
/// {
|
||||
/// "identifier": "main-user-files-write",
|
||||
/// "description": "This capability allows the `main` window on macOS and Windows access to `filesystem` write related commands and `dialog` commands to enable programmatic access to files selected by the user.",
|
||||
/// "windows": [
|
||||
/// "main"
|
||||
/// ],
|
||||
/// "permissions": [
|
||||
/// "core:default",
|
||||
/// "dialog:open",
|
||||
/// {
|
||||
/// "identifier": "fs:allow-write-text-file",
|
||||
/// "allow": [{ "path": "$HOME/test.txt" }]
|
||||
/// },
|
||||
/// ],
|
||||
/// "platforms": ["macOS","windows"]
|
||||
/// }
|
||||
/// ```
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
|
||||
pub struct Capability {
|
||||
/// Identifier of the capability.
|
||||
///
|
||||
/// ## Example
|
||||
///
|
||||
/// `main-user-files-write`
|
||||
///
|
||||
pub identifier: String,
|
||||
/// Description of what the capability is intended to allow on associated windows.
|
||||
///
|
||||
/// It should contain a description of what the grouped permissions should allow.
|
||||
///
|
||||
/// ## Example
|
||||
///
|
||||
/// This capability allows the `main` window access to `filesystem` write related
|
||||
/// commands and `dialog` commands to enable programmatic access to files selected by the user.
|
||||
#[serde(default)]
|
||||
pub description: String,
|
||||
/// Configure remote URLs that can use the capability permissions.
|
||||
///
|
||||
/// This setting is optional and defaults to not being set, as our
|
||||
/// default use case is that the content is served from our local application.
|
||||
///
|
||||
/// :::caution
|
||||
/// Make sure you understand the security implications of providing remote
|
||||
/// sources with local system access.
|
||||
/// :::
|
||||
///
|
||||
/// ## Example
|
||||
///
|
||||
/// ```json
|
||||
/// {
|
||||
/// "urls": ["https://*.mydomain.dev"]
|
||||
/// }
|
||||
/// ```
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub remote: Option<CapabilityRemote>,
|
||||
/// Whether this capability is enabled for local app URLs or not. Defaults to `true`.
|
||||
#[serde(default = "default_capability_local")]
|
||||
pub local: bool,
|
||||
/// List of windows that are affected by this capability. Can be a glob pattern.
|
||||
///
|
||||
/// If a window label matches any of the patterns in this list,
|
||||
/// the capability will be enabled on all the webviews of that window,
|
||||
/// regardless of the value of [`Self::webviews`].
|
||||
///
|
||||
/// On multiwebview windows, prefer specifying [`Self::webviews`] and omitting [`Self::windows`]
|
||||
/// for a fine grained access control.
|
||||
///
|
||||
/// ## Example
|
||||
///
|
||||
/// `["main"]`
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub windows: Vec<String>,
|
||||
/// List of webviews that are affected by this capability. Can be a glob pattern.
|
||||
///
|
||||
/// The capability will be enabled on all the webviews
|
||||
/// whose label matches any of the patterns in this list,
|
||||
/// regardless of whether the webview's window label matches a pattern in [`Self::windows`].
|
||||
///
|
||||
/// ## Example
|
||||
///
|
||||
/// `["sub-webview-one", "sub-webview-two"]`
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub webviews: Vec<String>,
|
||||
/// List of permissions attached to this capability.
|
||||
///
|
||||
/// Must include the plugin name as prefix in the form of `${plugin-name}:${permission-name}`.
|
||||
/// For commands directly implemented in the application itself only `${permission-name}`
|
||||
/// is required.
|
||||
///
|
||||
/// ## Example
|
||||
///
|
||||
/// ```json
|
||||
/// [
|
||||
/// "core:default",
|
||||
/// "shell:allow-open",
|
||||
/// "dialog:open",
|
||||
/// {
|
||||
/// "identifier": "fs:allow-write-text-file",
|
||||
/// "allow": [{ "path": "$HOME/test.txt" }]
|
||||
/// }
|
||||
/// ]
|
||||
/// ```
|
||||
#[cfg_attr(feature = "schema", schemars(schema_with = "unique_permission"))]
|
||||
pub permissions: Vec<PermissionEntry>,
|
||||
/// Limit which target platforms this capability applies to.
|
||||
///
|
||||
/// By default all platforms are targeted.
|
||||
///
|
||||
/// ## Example
|
||||
///
|
||||
/// `["macOS","windows"]`
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub platforms: Option<Vec<Target>>,
|
||||
}
|
||||
|
||||
impl Capability {
|
||||
/// Whether this capability should be active based on the platform target or not.
|
||||
pub fn is_active(&self, target: &Target) -> bool {
|
||||
self
|
||||
.platforms
|
||||
.as_ref()
|
||||
.map(|platforms| platforms.contains(target))
|
||||
.unwrap_or(true)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "schema")]
|
||||
fn unique_permission(generator: &mut schemars::SchemaGenerator) -> schemars::Schema {
|
||||
let items = serde_json::Value::from(generator.subschema_for::<PermissionEntry>());
|
||||
schemars::json_schema!({
|
||||
"type": "array",
|
||||
"uniqueItems": true,
|
||||
"items": items
|
||||
})
|
||||
}
|
||||
|
||||
fn default_capability_local() -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
/// Configuration for remote URLs that are associated with the capability.
|
||||
#[derive(Debug, Default, Clone, Serialize, Deserialize, Eq, PartialEq, PartialOrd, Ord, Hash)]
|
||||
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct CapabilityRemote {
|
||||
/// Remote domains this capability refers to using the [URLPattern standard](https://urlpattern.spec.whatwg.org/).
|
||||
///
|
||||
/// ## Examples
|
||||
///
|
||||
/// - "https://*.mydomain.dev": allows subdomains of mydomain.dev
|
||||
/// - "https://mydomain.dev/api/*": allows any subpath of mydomain.dev/api
|
||||
pub urls: Vec<String>,
|
||||
}
|
||||
|
||||
/// Capability formats accepted in a capability file.
|
||||
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
|
||||
#[cfg_attr(feature = "schema", schemars(untagged))]
|
||||
#[cfg_attr(test, derive(Debug, PartialEq))]
|
||||
pub enum CapabilityFile {
|
||||
/// A single capability.
|
||||
Capability(Capability),
|
||||
/// A list of capabilities.
|
||||
List(Vec<Capability>),
|
||||
/// A list of capabilities.
|
||||
NamedList {
|
||||
/// The list of capabilities.
|
||||
capabilities: Vec<Capability>,
|
||||
},
|
||||
}
|
||||
|
||||
impl CapabilityFile {
|
||||
/// Load the given capability file.
|
||||
pub fn load<P: AsRef<Path>>(path: P) -> Result<Self, super::Error> {
|
||||
let path = path.as_ref();
|
||||
let capability_file =
|
||||
std::fs::read_to_string(path).map_err(|e| super::Error::ReadFile(e, path.into()))?;
|
||||
let ext = path.extension().unwrap().to_string_lossy().to_string();
|
||||
let file: Self = match ext.as_str() {
|
||||
"toml" => toml::from_str(&capability_file)?,
|
||||
"json" => serde_json::from_str(&capability_file)?,
|
||||
#[cfg(feature = "config-json5")]
|
||||
"json5" => json5::from_str(&capability_file)?,
|
||||
_ => return Err(super::Error::UnknownCapabilityFormat(ext)),
|
||||
};
|
||||
Ok(file)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for CapabilityFile {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
UntaggedEnumVisitor::new()
|
||||
.seq(|seq| seq.deserialize::<Vec<Capability>>().map(Self::List))
|
||||
.map(|map| {
|
||||
#[derive(Deserialize)]
|
||||
struct CapabilityNamedList {
|
||||
capabilities: Vec<Capability>,
|
||||
}
|
||||
|
||||
let value: serde_json::Map<String, serde_json::Value> = map.deserialize()?;
|
||||
if value.contains_key("capabilities") {
|
||||
serde_json::from_value::<CapabilityNamedList>(value.into())
|
||||
.map(|named| Self::NamedList {
|
||||
capabilities: named.capabilities,
|
||||
})
|
||||
.map_err(|e| serde_untagged::de::Error::custom(e.to_string()))
|
||||
} else {
|
||||
serde_json::from_value::<Capability>(value.into())
|
||||
.map(Self::Capability)
|
||||
.map_err(|e| serde_untagged::de::Error::custom(e.to_string()))
|
||||
}
|
||||
})
|
||||
.deserialize(deserializer)
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for CapabilityFile {
|
||||
type Err = super::Error;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
serde_json::from_str(s)
|
||||
.or_else(|_| toml::from_str(s))
|
||||
.map_err(Into::into)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(feature = "build", feature = "build-2"))]
|
||||
mod build {
|
||||
use std::convert::identity;
|
||||
|
||||
use proc_macro2::TokenStream;
|
||||
use quote::{ToTokens, TokenStreamExt, quote};
|
||||
|
||||
use super::*;
|
||||
use crate::{literal_struct, tokens::*};
|
||||
|
||||
impl ToTokens for CapabilityRemote {
|
||||
fn to_tokens(&self, tokens: &mut TokenStream) {
|
||||
let urls = vec_lit(&self.urls, str_lit);
|
||||
literal_struct!(
|
||||
tokens,
|
||||
::tauri::utils::acl::capability::CapabilityRemote,
|
||||
urls
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
impl ToTokens for PermissionEntry {
|
||||
fn to_tokens(&self, tokens: &mut TokenStream) {
|
||||
let prefix = quote! { ::tauri::utils::acl::capability::PermissionEntry };
|
||||
|
||||
tokens.append_all(match self {
|
||||
Self::PermissionRef(id) => {
|
||||
quote! { #prefix::PermissionRef(#id) }
|
||||
}
|
||||
Self::ExtendedPermission { identifier, scope } => {
|
||||
quote! { #prefix::ExtendedPermission {
|
||||
identifier: #identifier,
|
||||
scope: #scope
|
||||
} }
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl ToTokens for Capability {
|
||||
fn to_tokens(&self, tokens: &mut TokenStream) {
|
||||
let identifier = str_lit(&self.identifier);
|
||||
let description = str_lit(&self.description);
|
||||
let remote = opt_lit(self.remote.as_ref());
|
||||
let local = self.local;
|
||||
let windows = vec_lit(&self.windows, str_lit);
|
||||
let webviews = vec_lit(&self.webviews, str_lit);
|
||||
let permissions = vec_lit(&self.permissions, identity);
|
||||
let platforms = opt_vec_lit(self.platforms.as_ref(), identity);
|
||||
|
||||
literal_struct!(
|
||||
tokens,
|
||||
::tauri::utils::acl::capability::Capability,
|
||||
identifier,
|
||||
description,
|
||||
remote,
|
||||
local,
|
||||
windows,
|
||||
webviews,
|
||||
permissions,
|
||||
platforms
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::acl::{Identifier, Scopes};
|
||||
|
||||
use super::{Capability, CapabilityFile, PermissionEntry};
|
||||
|
||||
#[test]
|
||||
fn permission_entry_de() {
|
||||
let identifier = Identifier::try_from("plugin:perm".to_string()).unwrap();
|
||||
let identifier_json = serde_json::to_string(&identifier).unwrap();
|
||||
assert_eq!(
|
||||
serde_json::from_str::<PermissionEntry>(&identifier_json).unwrap(),
|
||||
PermissionEntry::PermissionRef(identifier.clone())
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
serde_json::from_value::<PermissionEntry>(serde_json::json!({
|
||||
"identifier": identifier,
|
||||
"allow": [],
|
||||
"deny": null
|
||||
}))
|
||||
.unwrap(),
|
||||
PermissionEntry::ExtendedPermission {
|
||||
identifier,
|
||||
scope: Scopes {
|
||||
allow: Some(vec![]),
|
||||
deny: None
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn capability_file_de() {
|
||||
let capability = Capability {
|
||||
identifier: "test".into(),
|
||||
description: "".into(),
|
||||
remote: None,
|
||||
local: true,
|
||||
windows: vec![],
|
||||
webviews: vec![],
|
||||
permissions: vec![],
|
||||
platforms: None,
|
||||
};
|
||||
let capability_json = serde_json::to_string(&capability).unwrap();
|
||||
|
||||
assert_eq!(
|
||||
serde_json::from_str::<CapabilityFile>(&capability_json).unwrap(),
|
||||
CapabilityFile::Capability(capability.clone())
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
serde_json::from_str::<CapabilityFile>(&format!("[{capability_json}]")).unwrap(),
|
||||
CapabilityFile::List(vec![capability.clone()])
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
serde_json::from_str::<CapabilityFile>(&format!(
|
||||
"{{ \"capabilities\": [{capability_json}] }}"
|
||||
))
|
||||
.unwrap(),
|
||||
CapabilityFile::NamedList {
|
||||
capabilities: vec![capability]
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
301
apps/tauri/src-tauri/vendor/tauri-utils/src/acl/identifier.rs
vendored
Normal file
301
apps/tauri/src-tauri/vendor/tauri-utils/src/acl/identifier.rs
vendored
Normal file
|
|
@ -0,0 +1,301 @@
|
|||
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
//! Identifier for plugins.
|
||||
|
||||
use serde::{Deserialize, Deserializer, Serialize, Serializer};
|
||||
use std::num::NonZeroU8;
|
||||
use thiserror::Error;
|
||||
|
||||
const IDENTIFIER_SEPARATOR: u8 = b':';
|
||||
const PLUGIN_PREFIX: &str = "tauri-plugin-";
|
||||
const CORE_PLUGIN_IDENTIFIER_PREFIX: &str = "core:";
|
||||
|
||||
// <https://doc.rust-lang.org/cargo/reference/manifest.html#the-name-field>
|
||||
const MAX_LEN_PREFIX: usize = 64 - PLUGIN_PREFIX.len();
|
||||
const MAX_LEN_BASE: usize = 64;
|
||||
const MAX_LEN_IDENTIFIER: usize = MAX_LEN_PREFIX + 1 + MAX_LEN_BASE;
|
||||
|
||||
/// Plugin identifier.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct Identifier {
|
||||
inner: String,
|
||||
separator: Option<NonZeroU8>,
|
||||
}
|
||||
|
||||
#[cfg(feature = "schema")]
|
||||
impl schemars::JsonSchema for Identifier {
|
||||
fn schema_name() -> std::borrow::Cow<'static, str> {
|
||||
"Identifier".into()
|
||||
}
|
||||
|
||||
fn schema_id() -> std::borrow::Cow<'static, str> {
|
||||
std::borrow::Cow::Borrowed(concat!(module_path!(), "::Identifier"))
|
||||
}
|
||||
|
||||
fn json_schema(generator: &mut schemars::SchemaGenerator) -> schemars::Schema {
|
||||
String::json_schema(generator)
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<str> for Identifier {
|
||||
#[inline(always)]
|
||||
fn as_ref(&self) -> &str {
|
||||
&self.inner
|
||||
}
|
||||
}
|
||||
|
||||
impl Identifier {
|
||||
/// Get the identifier str.
|
||||
#[inline(always)]
|
||||
pub fn get(&self) -> &str {
|
||||
self.as_ref()
|
||||
}
|
||||
|
||||
/// Get the identifier without prefix.
|
||||
pub fn get_base(&self) -> &str {
|
||||
match self.separator_index() {
|
||||
None => self.get(),
|
||||
Some(i) => &self.inner[i + 1..],
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the prefix of the identifier.
|
||||
pub fn get_prefix(&self) -> Option<&str> {
|
||||
self.separator_index().map(|i| &self.inner[0..i])
|
||||
}
|
||||
|
||||
/// Set the identifier prefix.
|
||||
pub fn set_prefix(&mut self) -> Result<(), ParseIdentifierError> {
|
||||
todo!()
|
||||
}
|
||||
|
||||
/// Get the identifier string and its separator.
|
||||
pub fn into_inner(self) -> (String, Option<NonZeroU8>) {
|
||||
(self.inner, self.separator)
|
||||
}
|
||||
|
||||
fn separator_index(&self) -> Option<usize> {
|
||||
self.separator.map(|i| i.get() as usize)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
enum ValidByte {
|
||||
Separator,
|
||||
Byte(u8),
|
||||
}
|
||||
|
||||
impl ValidByte {
|
||||
fn alpha_numeric(byte: u8) -> Option<Self> {
|
||||
byte.is_ascii_alphanumeric().then_some(Self::Byte(byte))
|
||||
}
|
||||
|
||||
fn alpha_numeric_hyphen(byte: u8) -> Option<Self> {
|
||||
(byte.is_ascii_alphanumeric() || byte == b'-').then_some(Self::Byte(byte))
|
||||
}
|
||||
|
||||
fn next(&self, next: u8) -> Option<ValidByte> {
|
||||
match (self, next) {
|
||||
(ValidByte::Byte(b'-'), IDENTIFIER_SEPARATOR) => None,
|
||||
(ValidByte::Separator, b'-') => None,
|
||||
|
||||
(_, IDENTIFIER_SEPARATOR) => Some(ValidByte::Separator),
|
||||
(ValidByte::Separator, next) => ValidByte::alpha_numeric(next),
|
||||
(ValidByte::Byte(b'-'), next) => ValidByte::alpha_numeric_hyphen(next),
|
||||
(ValidByte::Byte(b'_'), next) => ValidByte::alpha_numeric_hyphen(next),
|
||||
(ValidByte::Byte(_), next) => ValidByte::alpha_numeric_hyphen(next),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Errors that can happen when parsing an identifier.
|
||||
#[derive(Debug, Error)]
|
||||
pub enum ParseIdentifierError {
|
||||
/// Identifier start with the plugin prefix.
|
||||
#[error("identifiers cannot start with {}", PLUGIN_PREFIX)]
|
||||
StartsWithTauriPlugin,
|
||||
|
||||
/// Identifier empty.
|
||||
#[error("identifiers cannot be empty")]
|
||||
Empty,
|
||||
|
||||
/// Identifier is too long.
|
||||
#[error("identifiers cannot be longer than {len}, found {0}", len = MAX_LEN_IDENTIFIER)]
|
||||
Humongous(usize),
|
||||
|
||||
/// Identifier is not in a valid format.
|
||||
#[error(
|
||||
"identifiers can only include lowercase ASCII, hyphens which are not leading or trailing, and a single colon if using a prefix"
|
||||
)]
|
||||
InvalidFormat,
|
||||
|
||||
/// Identifier has multiple separators.
|
||||
#[error(
|
||||
"identifiers can only include a single separator '{}'",
|
||||
IDENTIFIER_SEPARATOR
|
||||
)]
|
||||
MultipleSeparators,
|
||||
|
||||
/// Identifier has a trailing hyphen.
|
||||
#[error("identifiers cannot have a trailing hyphen")]
|
||||
TrailingHyphen,
|
||||
|
||||
/// Identifier has a prefix without a base.
|
||||
#[error("identifiers cannot have a prefix without a base")]
|
||||
PrefixWithoutBase,
|
||||
}
|
||||
|
||||
impl TryFrom<String> for Identifier {
|
||||
type Error = ParseIdentifierError;
|
||||
|
||||
fn try_from(value: String) -> Result<Self, Self::Error> {
|
||||
if value.starts_with(PLUGIN_PREFIX) {
|
||||
return Err(Self::Error::StartsWithTauriPlugin);
|
||||
}
|
||||
|
||||
if value.is_empty() {
|
||||
return Err(Self::Error::Empty);
|
||||
}
|
||||
|
||||
if value.len() > MAX_LEN_IDENTIFIER {
|
||||
return Err(Self::Error::Humongous(value.len()));
|
||||
}
|
||||
|
||||
let is_core_identifier = value.starts_with(CORE_PLUGIN_IDENTIFIER_PREFIX);
|
||||
|
||||
let mut bytes = value.bytes();
|
||||
|
||||
// grab the first byte only before parsing the rest
|
||||
let mut prev = bytes
|
||||
.next()
|
||||
.and_then(ValidByte::alpha_numeric)
|
||||
.ok_or(Self::Error::InvalidFormat)?;
|
||||
|
||||
let mut idx = 0;
|
||||
let mut separator = None;
|
||||
for byte in bytes {
|
||||
idx += 1; // we already consumed first item
|
||||
match prev.next(byte) {
|
||||
None => return Err(Self::Error::InvalidFormat),
|
||||
Some(next @ ValidByte::Byte(_)) => prev = next,
|
||||
Some(ValidByte::Separator) => {
|
||||
if separator.is_none() || is_core_identifier {
|
||||
// safe to unwrap because idx starts at 1 and cannot go over MAX_IDENTIFIER_LEN
|
||||
separator = Some(idx.try_into().unwrap());
|
||||
prev = ValidByte::Separator
|
||||
} else {
|
||||
return Err(Self::Error::MultipleSeparators);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
match prev {
|
||||
// empty base
|
||||
ValidByte::Separator => return Err(Self::Error::PrefixWithoutBase),
|
||||
|
||||
// trailing hyphen
|
||||
ValidByte::Byte(b'-') => return Err(Self::Error::TrailingHyphen),
|
||||
|
||||
_ => (),
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
inner: value,
|
||||
separator,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for Identifier {
|
||||
fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
Self::try_from(String::deserialize(deserializer)?).map_err(serde::de::Error::custom)
|
||||
}
|
||||
}
|
||||
|
||||
impl Serialize for Identifier {
|
||||
fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
serializer.serialize_str(self.get())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn ident(s: impl Into<String>) -> Result<Identifier, ParseIdentifierError> {
|
||||
Identifier::try_from(s.into())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn max_len_fits_in_u8() {
|
||||
assert!(MAX_LEN_IDENTIFIER < u8::MAX as usize)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn format() {
|
||||
assert!(ident("prefix:base").is_ok());
|
||||
assert!(ident("prefix3:base").is_ok());
|
||||
assert!(ident("preFix:base").is_ok());
|
||||
|
||||
// bad
|
||||
assert!(ident("tauri-plugin-prefix:base").is_err());
|
||||
|
||||
assert!(ident("-prefix-:-base-").is_err());
|
||||
assert!(ident("-prefix:base").is_err());
|
||||
assert!(ident("prefix-:base").is_err());
|
||||
assert!(ident("prefix:-base").is_err());
|
||||
assert!(ident("prefix:base-").is_err());
|
||||
|
||||
assert!(ident("pre--fix:base--sep").is_ok());
|
||||
assert!(ident("prefix:base--sep").is_ok());
|
||||
assert!(ident("pre--fix:base").is_ok());
|
||||
|
||||
assert!(ident("prefix::base").is_err());
|
||||
assert!(ident(":base").is_err());
|
||||
assert!(ident("prefix:").is_err());
|
||||
assert!(ident(":prefix:base:").is_err());
|
||||
assert!(ident("base:").is_err());
|
||||
|
||||
assert!(ident("").is_err());
|
||||
assert!(ident("💩").is_err());
|
||||
|
||||
assert!(ident("a".repeat(MAX_LEN_IDENTIFIER + 1)).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn base() {
|
||||
assert_eq!(ident("prefix:base").unwrap().get_base(), "base");
|
||||
assert_eq!(ident("base").unwrap().get_base(), "base");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn prefix() {
|
||||
assert_eq!(ident("prefix:base").unwrap().get_prefix(), Some("prefix"));
|
||||
assert_eq!(ident("base").unwrap().get_prefix(), None);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(feature = "build", feature = "build-2"))]
|
||||
mod build {
|
||||
use proc_macro2::TokenStream;
|
||||
use quote::{ToTokens, TokenStreamExt, quote};
|
||||
|
||||
use super::*;
|
||||
|
||||
impl ToTokens for Identifier {
|
||||
fn to_tokens(&self, tokens: &mut TokenStream) {
|
||||
let s = self.get();
|
||||
tokens
|
||||
.append_all(quote! { ::tauri::utils::acl::Identifier::try_from(#s.to_string()).unwrap() })
|
||||
}
|
||||
}
|
||||
}
|
||||
196
apps/tauri/src-tauri/vendor/tauri-utils/src/acl/manifest.rs
vendored
Normal file
196
apps/tauri/src-tauri/vendor/tauri-utils/src/acl/manifest.rs
vendored
Normal file
|
|
@ -0,0 +1,196 @@
|
|||
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
//! Plugin ACL types.
|
||||
|
||||
use std::{collections::BTreeMap, num::NonZeroU64};
|
||||
|
||||
use super::{Permission, PermissionSet};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// The default permission set of the plugin.
|
||||
///
|
||||
/// Works similarly to a permission with the "default" identifier.
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
|
||||
pub struct DefaultPermission {
|
||||
/// The version of the permission.
|
||||
pub version: Option<NonZeroU64>,
|
||||
|
||||
/// Human-readable description of what the permission does.
|
||||
/// Tauri convention is to use `<h4>` headings in markdown content
|
||||
/// for Tauri documentation generation purposes.
|
||||
pub description: Option<String>,
|
||||
|
||||
/// All permissions this set contains.
|
||||
pub permissions: Vec<String>,
|
||||
}
|
||||
|
||||
/// Permission file that can define a default permission, a set of permissions or a list of inlined permissions.
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
|
||||
pub struct PermissionFile {
|
||||
/// The default permission set for the plugin
|
||||
pub default: Option<DefaultPermission>,
|
||||
|
||||
/// A list of permissions sets defined
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub set: Vec<PermissionSet>,
|
||||
|
||||
/// A list of inlined permissions
|
||||
#[serde(default)]
|
||||
pub permission: Vec<Permission>,
|
||||
}
|
||||
|
||||
/// Plugin manifest.
|
||||
#[derive(Debug, Serialize, Deserialize, Default)]
|
||||
pub struct Manifest {
|
||||
/// Default permission.
|
||||
pub default_permission: Option<PermissionSet>,
|
||||
/// Plugin permissions.
|
||||
pub permissions: BTreeMap<String, Permission>,
|
||||
/// Plugin permission sets.
|
||||
pub permission_sets: BTreeMap<String, PermissionSet>,
|
||||
/// The global scope schema.
|
||||
pub global_scope_schema: Option<serde_json::Value>,
|
||||
}
|
||||
|
||||
impl Manifest {
|
||||
/// Creates a new manifest from the given plugin permission files and global scope schema.
|
||||
pub fn new(
|
||||
permission_files: Vec<PermissionFile>,
|
||||
global_scope_schema: Option<serde_json::Value>,
|
||||
) -> Self {
|
||||
let mut manifest = Self {
|
||||
default_permission: None,
|
||||
permissions: BTreeMap::new(),
|
||||
permission_sets: BTreeMap::new(),
|
||||
global_scope_schema,
|
||||
};
|
||||
|
||||
for permission_file in permission_files {
|
||||
if let Some(default) = permission_file.default {
|
||||
manifest.default_permission.replace(PermissionSet {
|
||||
identifier: "default".into(),
|
||||
description: default
|
||||
.description
|
||||
.unwrap_or_else(|| "Default plugin permissions.".to_string()),
|
||||
permissions: default.permissions,
|
||||
});
|
||||
}
|
||||
|
||||
for permission in permission_file.permission {
|
||||
let key = permission.identifier.clone();
|
||||
manifest.permissions.insert(key, permission);
|
||||
}
|
||||
|
||||
for set in permission_file.set {
|
||||
let key = set.identifier.clone();
|
||||
manifest.permission_sets.insert(key, set);
|
||||
}
|
||||
}
|
||||
|
||||
manifest
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "schema")]
|
||||
type ScopeSchema = (schemars::Schema, serde_json::Map<String, serde_json::Value>);
|
||||
|
||||
#[cfg(feature = "schema")]
|
||||
impl Manifest {
|
||||
/// Return scope schema and extra schema definitions for this plugin manifest.
|
||||
pub fn global_scope_schema(&self) -> Result<Option<ScopeSchema>, super::Error> {
|
||||
self
|
||||
.global_scope_schema
|
||||
.as_ref()
|
||||
.map(|s| {
|
||||
serde_json::from_value::<schemars::Schema>(s.clone()).map(|mut root| {
|
||||
// Extract definitions from the schema
|
||||
let definitions = root
|
||||
.remove("$defs")
|
||||
.or_else(|| root.remove("definitions"))
|
||||
.and_then(|v| match v {
|
||||
serde_json::Value::Object(m) => Some(m),
|
||||
_ => None,
|
||||
})
|
||||
.unwrap_or_default();
|
||||
|
||||
// Wrap in an array schema
|
||||
let items = serde_json::Value::from(root);
|
||||
let scope_schema = schemars::json_schema!({
|
||||
"type": "array",
|
||||
"items": items
|
||||
});
|
||||
|
||||
(scope_schema, definitions)
|
||||
})
|
||||
})
|
||||
.transpose()
|
||||
.map_err(Into::into)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(feature = "build", feature = "build-2"))]
|
||||
mod build {
|
||||
use proc_macro2::TokenStream;
|
||||
use quote::{ToTokens, TokenStreamExt, quote};
|
||||
use std::convert::identity;
|
||||
|
||||
use super::*;
|
||||
use crate::{literal_struct, tokens::*};
|
||||
|
||||
impl ToTokens for DefaultPermission {
|
||||
fn to_tokens(&self, tokens: &mut TokenStream) {
|
||||
let version = opt_lit_owned(self.version.as_ref().map(|v| {
|
||||
let v = v.get();
|
||||
quote!(::core::num::NonZeroU64::new(#v).unwrap())
|
||||
}));
|
||||
// Only used in build script and macros, so don't include them in runtime
|
||||
let description = quote! { ::core::option::Option::None };
|
||||
let permissions = vec_lit(&self.permissions, str_lit);
|
||||
literal_struct!(
|
||||
tokens,
|
||||
::tauri::utils::acl::plugin::DefaultPermission,
|
||||
version,
|
||||
description,
|
||||
permissions
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl ToTokens for Manifest {
|
||||
fn to_tokens(&self, tokens: &mut TokenStream) {
|
||||
let default_permission = opt_lit(self.default_permission.as_ref());
|
||||
|
||||
let permissions = map_lit(
|
||||
quote! { ::std::collections::BTreeMap },
|
||||
&self.permissions,
|
||||
str_lit,
|
||||
identity,
|
||||
);
|
||||
|
||||
let permission_sets = map_lit(
|
||||
quote! { ::std::collections::BTreeMap },
|
||||
&self.permission_sets,
|
||||
str_lit,
|
||||
identity,
|
||||
);
|
||||
|
||||
// Only used in build script and macros, so don't include them in runtime
|
||||
// let global_scope_schema =
|
||||
// opt_lit_owned(self.global_scope_schema.as_ref().map(json_value_lit));
|
||||
let global_scope_schema = quote! { ::core::option::Option::None };
|
||||
|
||||
literal_struct!(
|
||||
tokens,
|
||||
::tauri::utils::acl::manifest::Manifest,
|
||||
default_permission,
|
||||
permissions,
|
||||
permission_sets,
|
||||
global_scope_schema
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
550
apps/tauri/src-tauri/vendor/tauri-utils/src/acl/mod.rs
vendored
Normal file
550
apps/tauri/src-tauri/vendor/tauri-utils/src/acl/mod.rs
vendored
Normal file
|
|
@ -0,0 +1,550 @@
|
|||
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
//! Access Control List types.
|
||||
//!
|
||||
//! # Stability
|
||||
//!
|
||||
//! This is a core functionality that is not considered part of the stable API.
|
||||
//! If you use it, note that it may include breaking changes in the future.
|
||||
//!
|
||||
//! These items are intended to be non-breaking from a de/serialization standpoint only.
|
||||
//! Using and modifying existing config values will try to avoid breaking changes, but they are
|
||||
//! free to add fields in the future - causing breaking changes for creating and full destructuring.
|
||||
//!
|
||||
//! To avoid this, [ignore unknown fields when destructuring] with the `{my, config, ..}` pattern.
|
||||
//! If you need to create the Rust config directly without deserializing, then create the struct
|
||||
//! the [Struct Update Syntax] with `..Default::default()`, which may need a
|
||||
//! `#[allow(clippy::needless_update)]` attribute if you are declaring all fields.
|
||||
//!
|
||||
//! [ignore unknown fields when destructuring]: https://doc.rust-lang.org/book/ch18-03-pattern-syntax.html#ignoring-remaining-parts-of-a-value-with-
|
||||
//! [Struct Update Syntax]: https://doc.rust-lang.org/book/ch05-01-defining-structs.html#creating-instances-from-other-instances-with-struct-update-syntax
|
||||
|
||||
use anyhow::Context;
|
||||
use capability::{Capability, CapabilityFile};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{
|
||||
collections::{BTreeMap, HashSet},
|
||||
fs,
|
||||
num::NonZeroU64,
|
||||
path::PathBuf,
|
||||
str::FromStr,
|
||||
sync::Arc,
|
||||
};
|
||||
use thiserror::Error;
|
||||
use url::Url;
|
||||
|
||||
use crate::{
|
||||
config::{CapabilityEntry, Config},
|
||||
platform::Target,
|
||||
};
|
||||
|
||||
pub use self::{identifier::*, value::*};
|
||||
|
||||
/// Known foldername of the permission schema files
|
||||
pub const PERMISSION_SCHEMAS_FOLDER_NAME: &str = "schemas";
|
||||
/// Known filename of the permission schema JSON file
|
||||
pub const PERMISSION_SCHEMA_FILE_NAME: &str = "schema.json";
|
||||
/// Known ACL key for the app permissions.
|
||||
pub const APP_ACL_KEY: &str = "__app-acl__";
|
||||
/// Known acl manifests file
|
||||
pub const ACL_MANIFESTS_FILE_NAME: &str = "acl-manifests.json";
|
||||
/// Known capabilities file
|
||||
pub const CAPABILITIES_FILE_NAME: &str = "capabilities.json";
|
||||
/// Allowed commands file name
|
||||
pub const ALLOWED_COMMANDS_FILE_NAME: &str = "allowed-commands.json";
|
||||
/// Set by the CLI with when `build > removeUnusedCommands` is set for dead code elimination,
|
||||
/// the value is set to the config's directory
|
||||
pub const REMOVE_UNUSED_COMMANDS_ENV_VAR: &str = "REMOVE_UNUSED_COMMANDS";
|
||||
|
||||
#[cfg(any(feature = "build", feature = "build-2"))]
|
||||
pub mod build;
|
||||
pub mod capability;
|
||||
pub mod identifier;
|
||||
pub mod manifest;
|
||||
pub mod resolved;
|
||||
#[cfg(feature = "schema")]
|
||||
pub mod schema;
|
||||
pub mod value;
|
||||
|
||||
/// Possible errors while processing ACL files.
|
||||
#[derive(Debug, Error)]
|
||||
pub enum Error {
|
||||
/// Could not find an environmental variable that is set inside of build scripts.
|
||||
///
|
||||
/// Whatever generated this should be called inside of a build script.
|
||||
#[error(
|
||||
"expected build script env var {0}, but it was not found - ensure this is called in a build script"
|
||||
)]
|
||||
BuildVar(&'static str),
|
||||
|
||||
/// The links field in the manifest **MUST** be set and match the name of the crate.
|
||||
#[error(
|
||||
"package.links field in the Cargo manifest is not set, it should be set to the same as package.name"
|
||||
)]
|
||||
LinksMissing,
|
||||
|
||||
/// The links field in the manifest **MUST** match the name of the crate.
|
||||
#[error(
|
||||
"package.links field in the Cargo manifest MUST be set to the same value as package.name"
|
||||
)]
|
||||
LinksName,
|
||||
|
||||
/// IO error while reading a file
|
||||
#[error("failed to read file '{}': {}", _1.display(), _0)]
|
||||
ReadFile(std::io::Error, PathBuf),
|
||||
|
||||
/// IO error while writing a file
|
||||
#[error("failed to write file '{}': {}", _1.display(), _0)]
|
||||
WriteFile(std::io::Error, PathBuf),
|
||||
|
||||
/// IO error while creating a file
|
||||
#[error("failed to create file '{}': {}", _1.display(), _0)]
|
||||
CreateFile(std::io::Error, PathBuf),
|
||||
|
||||
/// IO error while creating a dir
|
||||
#[error("failed to create dir '{}': {}", _1.display(), _0)]
|
||||
CreateDir(std::io::Error, PathBuf),
|
||||
|
||||
/// [`cargo_metadata`] was not able to complete successfully
|
||||
#[cfg(any(feature = "build", feature = "build-2"))]
|
||||
#[error("failed to execute: {0}")]
|
||||
Metadata(#[from] ::cargo_metadata::Error),
|
||||
|
||||
/// Invalid glob
|
||||
#[error("failed to run glob: {0}")]
|
||||
Glob(#[from] glob::PatternError),
|
||||
|
||||
/// Invalid TOML encountered
|
||||
#[error("failed to parse TOML: {0}")]
|
||||
Toml(#[from] toml::de::Error),
|
||||
|
||||
/// Invalid JSON encountered
|
||||
#[error("failed to parse JSON: {0}")]
|
||||
Json(#[from] serde_json::Error),
|
||||
|
||||
/// Invalid JSON5 encountered
|
||||
#[cfg(feature = "config-json5")]
|
||||
#[error("failed to parse JSON5: {0}")]
|
||||
Json5(#[from] json5::Error),
|
||||
|
||||
/// Invalid permissions file format
|
||||
#[error("unknown permission format {0}")]
|
||||
UnknownPermissionFormat(String),
|
||||
|
||||
/// Invalid capabilities file format
|
||||
#[error("unknown capability format {0}")]
|
||||
UnknownCapabilityFormat(String),
|
||||
|
||||
/// Permission referenced in set not found.
|
||||
#[error("permission {permission} not found from set {set}")]
|
||||
SetPermissionNotFound {
|
||||
/// Permission identifier.
|
||||
permission: String,
|
||||
/// Set identifier.
|
||||
set: String,
|
||||
},
|
||||
|
||||
/// Unknown ACL manifest.
|
||||
#[error("unknown ACL for {key}, expected one of {available}")]
|
||||
UnknownManifest {
|
||||
/// Manifest key.
|
||||
key: String,
|
||||
/// Available manifest keys.
|
||||
available: String,
|
||||
},
|
||||
|
||||
/// Unknown permission.
|
||||
#[error("unknown permission {permission} for {key}")]
|
||||
UnknownPermission {
|
||||
/// Manifest key.
|
||||
key: String,
|
||||
|
||||
/// Permission identifier.
|
||||
permission: String,
|
||||
},
|
||||
|
||||
/// Capability with the given identifier already exists.
|
||||
#[error("capability with identifier `{identifier}` already exists")]
|
||||
CapabilityAlreadyExists {
|
||||
/// Capability identifier.
|
||||
identifier: String,
|
||||
},
|
||||
}
|
||||
|
||||
/// Allowed and denied commands inside a permission.
|
||||
///
|
||||
/// If two commands clash inside of `allow` and `deny`, it should be denied by default.
|
||||
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
|
||||
pub struct Commands {
|
||||
/// Allowed command.
|
||||
#[serde(default)]
|
||||
pub allow: Vec<String>,
|
||||
|
||||
/// Denied command, which takes priority.
|
||||
#[serde(default)]
|
||||
pub deny: Vec<String>,
|
||||
}
|
||||
|
||||
/// An argument for fine grained behavior control of Tauri commands.
|
||||
///
|
||||
/// It 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.
|
||||
///
|
||||
/// ## Example
|
||||
///
|
||||
/// ```json
|
||||
/// {
|
||||
/// "allow": [{ "path": "$HOME/**" }],
|
||||
/// "deny": [{ "path": "$HOME/secret.txt" }]
|
||||
/// }
|
||||
/// ```
|
||||
#[derive(Debug, Default, PartialEq, Clone, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
|
||||
pub struct Scopes {
|
||||
/// Data that defines what is allowed by the scope.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub allow: Option<Vec<Value>>,
|
||||
/// Data that defines what is denied by the scope. This should be prioritized by validation logic.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub deny: Option<Vec<Value>>,
|
||||
}
|
||||
|
||||
impl Scopes {
|
||||
fn is_empty(&self) -> bool {
|
||||
self.allow.is_none() && self.deny.is_none()
|
||||
}
|
||||
}
|
||||
|
||||
/// Descriptions of explicit privileges of commands.
|
||||
///
|
||||
/// It can enable commands to be accessible in the frontend of the application.
|
||||
///
|
||||
/// If the scope is defined it can be used to fine grain control the access of individual or multiple commands.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
|
||||
pub struct Permission {
|
||||
/// The version of the permission.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub version: Option<NonZeroU64>,
|
||||
|
||||
/// A unique identifier for the permission.
|
||||
pub identifier: String,
|
||||
|
||||
/// Human-readable description of what the permission does.
|
||||
/// Tauri internal convention is to use `<h4>` headings in markdown content
|
||||
/// for Tauri documentation generation purposes.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub description: Option<String>,
|
||||
|
||||
/// Allowed or denied commands when using this permission.
|
||||
#[serde(default)]
|
||||
pub commands: Commands,
|
||||
|
||||
/// Allowed or denied scoped when using this permission.
|
||||
#[serde(default, skip_serializing_if = "Scopes::is_empty")]
|
||||
pub scope: Scopes,
|
||||
|
||||
/// Target platforms this permission applies. By default all platforms are affected by this permission.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub platforms: Option<Vec<Target>>,
|
||||
}
|
||||
|
||||
impl Permission {
|
||||
/// Whether this permission should be active based on the platform target or not.
|
||||
pub fn is_active(&self, target: &Target) -> bool {
|
||||
self
|
||||
.platforms
|
||||
.as_ref()
|
||||
.map(|platforms| platforms.contains(target))
|
||||
.unwrap_or(true)
|
||||
}
|
||||
}
|
||||
|
||||
/// A set of direct permissions grouped together under a new name.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
|
||||
pub struct PermissionSet {
|
||||
/// A unique identifier for the permission.
|
||||
pub identifier: String,
|
||||
|
||||
/// Human-readable description of what the permission does.
|
||||
pub description: String,
|
||||
|
||||
/// All permissions this set contains.
|
||||
pub permissions: Vec<String>,
|
||||
}
|
||||
|
||||
/// UrlPattern for [`ExecutionContext::Remote`].
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct RemoteUrlPattern(Arc<urlpattern::UrlPattern>, String);
|
||||
|
||||
impl FromStr for RemoteUrlPattern {
|
||||
type Err = urlpattern::quirks::Error;
|
||||
|
||||
fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
|
||||
let mut init = urlpattern::UrlPatternInit::parse_constructor_string::<regex::Regex>(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());
|
||||
}
|
||||
let pattern = urlpattern::UrlPattern::parse(init, Default::default())?;
|
||||
Ok(Self(Arc::new(pattern), s.to_string()))
|
||||
}
|
||||
}
|
||||
|
||||
impl RemoteUrlPattern {
|
||||
#[doc(hidden)]
|
||||
pub fn as_str(&self) -> &str {
|
||||
&self.1
|
||||
}
|
||||
|
||||
/// Test if a given URL matches the pattern.
|
||||
pub fn test(&self, url: &Url) -> bool {
|
||||
self
|
||||
.0
|
||||
.test(urlpattern::UrlPatternMatchInput::Url(url.clone()))
|
||||
.unwrap_or_default()
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq for RemoteUrlPattern {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.0.protocol() == other.0.protocol()
|
||||
&& self.0.username() == other.0.username()
|
||||
&& self.0.password() == other.0.password()
|
||||
&& self.0.hostname() == other.0.hostname()
|
||||
&& self.0.port() == other.0.port()
|
||||
&& self.0.pathname() == other.0.pathname()
|
||||
&& self.0.search() == other.0.search()
|
||||
&& self.0.hash() == other.0.hash()
|
||||
}
|
||||
}
|
||||
|
||||
impl Eq for RemoteUrlPattern {}
|
||||
|
||||
/// Execution context of an IPC call.
|
||||
#[derive(Debug, Default, Clone, Eq, PartialEq)]
|
||||
pub enum ExecutionContext {
|
||||
/// A local URL is used (the Tauri app URL).
|
||||
#[default]
|
||||
Local,
|
||||
/// Remote URL is trying to use the IPC.
|
||||
Remote {
|
||||
/// The URL trying to access the IPC (URL pattern).
|
||||
url: RemoteUrlPattern,
|
||||
},
|
||||
}
|
||||
|
||||
/// Test if the app has an application manifest from the ACL
|
||||
pub fn has_app_manifest(acl: &BTreeMap<String, crate::acl::manifest::Manifest>) -> bool {
|
||||
acl.contains_key(APP_ACL_KEY)
|
||||
}
|
||||
|
||||
/// Get the capabilities from the config file
|
||||
pub fn get_capabilities(
|
||||
config: &Config,
|
||||
mut capabilities_from_files: BTreeMap<String, Capability>,
|
||||
additional_capability_files: Option<&[PathBuf]>,
|
||||
) -> anyhow::Result<BTreeMap<String, Capability>> {
|
||||
let mut capabilities = if config.app.security.capabilities.is_empty() {
|
||||
capabilities_from_files
|
||||
} else {
|
||||
let mut capabilities = BTreeMap::new();
|
||||
for capability_entry in &config.app.security.capabilities {
|
||||
match capability_entry {
|
||||
CapabilityEntry::Inlined(capability) => {
|
||||
capabilities.insert(capability.identifier.clone(), capability.clone());
|
||||
}
|
||||
CapabilityEntry::Reference(id) => {
|
||||
let capability = capabilities_from_files
|
||||
.remove(id)
|
||||
.with_context(|| format!("capability with identifier {id} not found"))?;
|
||||
capabilities.insert(id.clone(), capability);
|
||||
}
|
||||
}
|
||||
}
|
||||
capabilities
|
||||
};
|
||||
|
||||
if let Some(paths) = additional_capability_files {
|
||||
for path in paths {
|
||||
let capability = CapabilityFile::load(path)
|
||||
.with_context(|| format!("failed to read capability {}", path.display()))?;
|
||||
match capability {
|
||||
CapabilityFile::Capability(c) => {
|
||||
capabilities.insert(c.identifier.clone(), c);
|
||||
}
|
||||
CapabilityFile::List(capabilities_list)
|
||||
| CapabilityFile::NamedList {
|
||||
capabilities: capabilities_list,
|
||||
} => {
|
||||
capabilities.extend(
|
||||
capabilities_list
|
||||
.into_iter()
|
||||
.map(|c| (c.identifier.clone(), c)),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(capabilities)
|
||||
}
|
||||
|
||||
/// Allowed commands used to communicate between `generate_handle` and `generate_allowed_commands` through json files
|
||||
#[derive(Debug, Default, Serialize, Deserialize)]
|
||||
pub struct AllowedCommands {
|
||||
/// The commands allowed
|
||||
pub commands: HashSet<String>,
|
||||
/// Has application ACL or not
|
||||
pub has_app_acl: bool,
|
||||
}
|
||||
|
||||
/// Try to reads allowed commands from the out dir made by our build script
|
||||
pub fn read_allowed_commands() -> Option<AllowedCommands> {
|
||||
let out_file = std::env::var("OUT_DIR")
|
||||
.map(PathBuf::from)
|
||||
.ok()?
|
||||
.join(ALLOWED_COMMANDS_FILE_NAME);
|
||||
let file = fs::read_to_string(&out_file).ok()?;
|
||||
let json = serde_json::from_str(&file).ok()?;
|
||||
Some(json)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::acl::RemoteUrlPattern;
|
||||
|
||||
#[test]
|
||||
fn url_pattern_domain_wildcard() {
|
||||
let pattern: RemoteUrlPattern = "http://*".parse().unwrap();
|
||||
|
||||
assert!(pattern.test(&"http://tauri.app/path".parse().unwrap()));
|
||||
assert!(pattern.test(&"http://tauri.app/path?q=1".parse().unwrap()));
|
||||
|
||||
assert!(pattern.test(&"http://localhost/path".parse().unwrap()));
|
||||
assert!(pattern.test(&"http://localhost/path?q=1".parse().unwrap()));
|
||||
|
||||
let pattern: RemoteUrlPattern = "http://*.tauri.app".parse().unwrap();
|
||||
|
||||
assert!(!pattern.test(&"http://tauri.app/path".parse().unwrap()));
|
||||
assert!(!pattern.test(&"http://tauri.app/path?q=1".parse().unwrap()));
|
||||
assert!(pattern.test(&"http://api.tauri.app/path".parse().unwrap()));
|
||||
assert!(pattern.test(&"http://api.tauri.app/path?q=1".parse().unwrap()));
|
||||
assert!(!pattern.test(&"http://localhost/path".parse().unwrap()));
|
||||
assert!(!pattern.test(&"http://localhost/path?q=1".parse().unwrap()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn url_pattern_path_wildcard() {
|
||||
let pattern: RemoteUrlPattern = "http://localhost/*".parse().unwrap();
|
||||
assert!(pattern.test(&"http://localhost/path".parse().unwrap()));
|
||||
assert!(pattern.test(&"http://localhost/path?q=1".parse().unwrap()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn url_pattern_scheme_wildcard() {
|
||||
let pattern: RemoteUrlPattern = "*://localhost".parse().unwrap();
|
||||
assert!(pattern.test(&"http://localhost/path".parse().unwrap()));
|
||||
assert!(pattern.test(&"https://localhost/path?q=1".parse().unwrap()));
|
||||
assert!(pattern.test(&"custom://localhost/path".parse().unwrap()));
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(feature = "build", feature = "build-2"))]
|
||||
mod build_ {
|
||||
use std::convert::identity;
|
||||
|
||||
use crate::{literal_struct, tokens::*};
|
||||
|
||||
use super::*;
|
||||
use proc_macro2::TokenStream;
|
||||
use quote::{ToTokens, TokenStreamExt, quote};
|
||||
|
||||
impl ToTokens for ExecutionContext {
|
||||
fn to_tokens(&self, tokens: &mut TokenStream) {
|
||||
let prefix = quote! { ::tauri::utils::acl::ExecutionContext };
|
||||
|
||||
tokens.append_all(match self {
|
||||
Self::Local => {
|
||||
quote! { #prefix::Local }
|
||||
}
|
||||
Self::Remote { url } => {
|
||||
let url = url.as_str();
|
||||
quote! { #prefix::Remote { url: #url.parse().unwrap() } }
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl ToTokens for Commands {
|
||||
fn to_tokens(&self, tokens: &mut TokenStream) {
|
||||
let allow = vec_lit(&self.allow, str_lit);
|
||||
let deny = vec_lit(&self.deny, str_lit);
|
||||
literal_struct!(tokens, ::tauri::utils::acl::Commands, allow, deny)
|
||||
}
|
||||
}
|
||||
|
||||
impl ToTokens for Scopes {
|
||||
fn to_tokens(&self, tokens: &mut TokenStream) {
|
||||
let allow = opt_vec_lit(self.allow.as_ref(), identity);
|
||||
let deny = opt_vec_lit(self.deny.as_ref(), identity);
|
||||
literal_struct!(tokens, ::tauri::utils::acl::Scopes, allow, deny)
|
||||
}
|
||||
}
|
||||
|
||||
impl ToTokens for Permission {
|
||||
fn to_tokens(&self, tokens: &mut TokenStream) {
|
||||
let version = opt_lit_owned(self.version.as_ref().map(|v| {
|
||||
let v = v.get();
|
||||
quote!(::core::num::NonZeroU64::new(#v).unwrap())
|
||||
}));
|
||||
let identifier = str_lit(&self.identifier);
|
||||
// Only used in build script and macros, so don't include them in runtime
|
||||
let description = quote! { ::core::option::Option::None };
|
||||
let commands = &self.commands;
|
||||
let scope = &self.scope;
|
||||
let platforms = opt_vec_lit(self.platforms.as_ref(), identity);
|
||||
|
||||
literal_struct!(
|
||||
tokens,
|
||||
::tauri::utils::acl::Permission,
|
||||
version,
|
||||
identifier,
|
||||
description,
|
||||
commands,
|
||||
scope,
|
||||
platforms
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl ToTokens for PermissionSet {
|
||||
fn to_tokens(&self, tokens: &mut TokenStream) {
|
||||
let identifier = str_lit(&self.identifier);
|
||||
// Only used in build script and macros, so don't include them in runtime
|
||||
let description = quote! { "".to_string() };
|
||||
let permissions = vec_lit(&self.permissions, str_lit);
|
||||
literal_struct!(
|
||||
tokens,
|
||||
::tauri::utils::acl::PermissionSet,
|
||||
identifier,
|
||||
description,
|
||||
permissions
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
683
apps/tauri/src-tauri/vendor/tauri-utils/src/acl/resolved.rs
vendored
Normal file
683
apps/tauri/src-tauri/vendor/tauri-utils/src/acl/resolved.rs
vendored
Normal file
|
|
@ -0,0 +1,683 @@
|
|||
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
//! Resolved ACL for runtime usage.
|
||||
|
||||
use std::{collections::BTreeMap, fmt};
|
||||
|
||||
use crate::platform::Target;
|
||||
|
||||
use super::{
|
||||
APP_ACL_KEY, Commands, Error, ExecutionContext, Identifier, Permission, PermissionSet, Scopes,
|
||||
Value,
|
||||
capability::{Capability, PermissionEntry},
|
||||
has_app_manifest,
|
||||
manifest::Manifest,
|
||||
};
|
||||
|
||||
/// A key for a scope, used to link a [`ResolvedCommand#structfield.scope`] to the store [`Resolved#structfield.scopes`].
|
||||
pub type ScopeKey = u64;
|
||||
|
||||
/// Metadata for what referenced a [`ResolvedCommand`].
|
||||
#[cfg(debug_assertions)]
|
||||
#[derive(Default, Clone, PartialEq, Eq)]
|
||||
pub struct ResolvedCommandReference {
|
||||
/// Identifier of the capability.
|
||||
pub capability: String,
|
||||
/// Identifier of the permission.
|
||||
pub permission: String,
|
||||
}
|
||||
|
||||
/// A resolved command permission.
|
||||
#[derive(Default, Clone, PartialEq, Eq)]
|
||||
pub struct ResolvedCommand {
|
||||
/// The execution context of this command.
|
||||
pub context: ExecutionContext,
|
||||
/// The capability/permission that referenced this command.
|
||||
#[cfg(debug_assertions)]
|
||||
pub referenced_by: ResolvedCommandReference,
|
||||
/// The list of window label patterns that was resolved for this command.
|
||||
pub windows: Vec<glob::Pattern>,
|
||||
/// The list of webview label patterns that was resolved for this command.
|
||||
pub webviews: Vec<glob::Pattern>,
|
||||
/// The reference of the scope that is associated with this command. See [`Resolved#structfield.command_scopes`].
|
||||
pub scope_id: Option<ScopeKey>,
|
||||
}
|
||||
|
||||
impl fmt::Debug for ResolvedCommand {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.debug_struct("ResolvedCommand")
|
||||
.field("context", &self.context)
|
||||
.field("windows", &self.windows)
|
||||
.field("webviews", &self.webviews)
|
||||
.field("scope_id", &self.scope_id)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
/// A resolved scope. Merges all scopes defined for a single command.
|
||||
#[derive(Debug, Default, Clone)]
|
||||
pub struct ResolvedScope {
|
||||
/// Allows something on the command.
|
||||
pub allow: Vec<Value>,
|
||||
/// Denies something on the command.
|
||||
pub deny: Vec<Value>,
|
||||
}
|
||||
|
||||
/// Resolved access control list.
|
||||
#[derive(Debug, Default)]
|
||||
pub struct Resolved {
|
||||
/// If we should check the ACL for the app commands
|
||||
pub has_app_acl: bool,
|
||||
/// The commands that are allowed. Map each command with its context to a [`ResolvedCommand`].
|
||||
pub allowed_commands: BTreeMap<String, Vec<ResolvedCommand>>,
|
||||
/// The commands that are denied. Map each command with its context to a [`ResolvedCommand`].
|
||||
pub denied_commands: BTreeMap<String, Vec<ResolvedCommand>>,
|
||||
/// The store of scopes referenced by a [`ResolvedCommand`].
|
||||
pub command_scope: BTreeMap<ScopeKey, ResolvedScope>,
|
||||
/// The global scope.
|
||||
pub global_scope: BTreeMap<String, ResolvedScope>,
|
||||
}
|
||||
|
||||
impl Resolved {
|
||||
/// Resolves the ACL for the given plugin permissions and app capabilities.
|
||||
pub fn resolve(
|
||||
acl: &BTreeMap<String, Manifest>,
|
||||
mut capabilities: BTreeMap<String, Capability>,
|
||||
target: Target,
|
||||
) -> Result<Self, Error> {
|
||||
let mut allowed_commands = BTreeMap::new();
|
||||
let mut denied_commands = BTreeMap::new();
|
||||
|
||||
let mut current_scope_id = 0;
|
||||
let mut command_scope = BTreeMap::new();
|
||||
let mut global_scope: BTreeMap<String, Vec<Scopes>> = BTreeMap::new();
|
||||
|
||||
// resolve commands
|
||||
for capability in capabilities.values_mut().filter(|c| c.is_active(&target)) {
|
||||
with_resolved_permissions(
|
||||
capability,
|
||||
acl,
|
||||
target,
|
||||
|ResolvedPermission {
|
||||
key,
|
||||
commands,
|
||||
scope,
|
||||
#[cfg_attr(not(debug_assertions), allow(unused))]
|
||||
permission_name,
|
||||
}| {
|
||||
if commands.allow.is_empty() && commands.deny.is_empty() {
|
||||
// global scope
|
||||
global_scope.entry(key.to_string()).or_default().push(scope);
|
||||
} else {
|
||||
let scope_id = if scope.allow.is_some() || scope.deny.is_some() {
|
||||
current_scope_id += 1;
|
||||
command_scope.insert(
|
||||
current_scope_id,
|
||||
ResolvedScope {
|
||||
allow: scope.allow.unwrap_or_default(),
|
||||
deny: scope.deny.unwrap_or_default(),
|
||||
},
|
||||
);
|
||||
Some(current_scope_id)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
for allowed_command in &commands.allow {
|
||||
resolve_command(
|
||||
&mut allowed_commands,
|
||||
if key == APP_ACL_KEY {
|
||||
allowed_command.to_string()
|
||||
} else if let Some(core_plugin_name) = key.strip_prefix("core:") {
|
||||
format!("plugin:{core_plugin_name}|{allowed_command}")
|
||||
} else {
|
||||
format!("plugin:{key}|{allowed_command}")
|
||||
},
|
||||
capability,
|
||||
scope_id,
|
||||
#[cfg(debug_assertions)]
|
||||
permission_name.to_string(),
|
||||
)?;
|
||||
}
|
||||
|
||||
for denied_command in &commands.deny {
|
||||
resolve_command(
|
||||
&mut denied_commands,
|
||||
if key == APP_ACL_KEY {
|
||||
denied_command.to_string()
|
||||
} else if let Some(core_plugin_name) = key.strip_prefix("core:") {
|
||||
format!("plugin:{core_plugin_name}|{denied_command}")
|
||||
} else {
|
||||
format!("plugin:{key}|{denied_command}")
|
||||
},
|
||||
capability,
|
||||
scope_id,
|
||||
#[cfg(debug_assertions)]
|
||||
permission_name.to_string(),
|
||||
)?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
},
|
||||
)?;
|
||||
}
|
||||
|
||||
let global_scope = global_scope
|
||||
.into_iter()
|
||||
.map(|(key, scopes)| {
|
||||
let mut resolved_scope = ResolvedScope {
|
||||
allow: Vec::new(),
|
||||
deny: Vec::new(),
|
||||
};
|
||||
for scope in scopes {
|
||||
if let Some(allow) = scope.allow {
|
||||
resolved_scope.allow.extend(allow);
|
||||
}
|
||||
if let Some(deny) = scope.deny {
|
||||
resolved_scope.deny.extend(deny);
|
||||
}
|
||||
}
|
||||
(key, resolved_scope)
|
||||
})
|
||||
.collect();
|
||||
|
||||
let resolved = Self {
|
||||
has_app_acl: has_app_manifest(acl),
|
||||
allowed_commands,
|
||||
denied_commands,
|
||||
command_scope,
|
||||
global_scope,
|
||||
};
|
||||
|
||||
Ok(resolved)
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_glob_patterns(mut raw: Vec<String>) -> Result<Vec<glob::Pattern>, Error> {
|
||||
raw.sort();
|
||||
|
||||
let mut patterns = Vec::new();
|
||||
for pattern in raw {
|
||||
patterns.push(glob::Pattern::new(&pattern)?);
|
||||
}
|
||||
|
||||
Ok(patterns)
|
||||
}
|
||||
|
||||
fn resolve_command(
|
||||
commands: &mut BTreeMap<String, Vec<ResolvedCommand>>,
|
||||
command: String,
|
||||
capability: &Capability,
|
||||
scope_id: Option<ScopeKey>,
|
||||
#[cfg(debug_assertions)] referenced_by_permission_identifier: String,
|
||||
) -> Result<(), Error> {
|
||||
let mut contexts = Vec::new();
|
||||
if capability.local {
|
||||
contexts.push(ExecutionContext::Local);
|
||||
}
|
||||
if let Some(remote) = &capability.remote {
|
||||
contexts.extend(remote.urls.iter().map(|url| {
|
||||
ExecutionContext::Remote {
|
||||
url: url
|
||||
.parse()
|
||||
.unwrap_or_else(|e| panic!("invalid URL pattern for remote URL {url}: {e}")),
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
for context in contexts {
|
||||
let resolved_list = commands.entry(command.clone()).or_default();
|
||||
|
||||
resolved_list.push(ResolvedCommand {
|
||||
context,
|
||||
#[cfg(debug_assertions)]
|
||||
referenced_by: ResolvedCommandReference {
|
||||
capability: capability.identifier.clone(),
|
||||
permission: referenced_by_permission_identifier.clone(),
|
||||
},
|
||||
windows: parse_glob_patterns(capability.windows.clone())?,
|
||||
webviews: parse_glob_patterns(capability.webviews.clone())?,
|
||||
scope_id,
|
||||
});
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
struct ResolvedPermission<'a> {
|
||||
key: &'a str,
|
||||
permission_name: &'a str,
|
||||
commands: Commands,
|
||||
scope: Scopes,
|
||||
}
|
||||
|
||||
/// Iterate over permissions in a capability, resolving permission sets if necessary
|
||||
/// to produce a [`ResolvedPermission`] and calling the provided callback with it.
|
||||
fn with_resolved_permissions<F: FnMut(ResolvedPermission<'_>) -> Result<(), Error>>(
|
||||
capability: &Capability,
|
||||
acl: &BTreeMap<String, Manifest>,
|
||||
target: Target,
|
||||
mut f: F,
|
||||
) -> Result<(), Error> {
|
||||
for permission_entry in &capability.permissions {
|
||||
let permission_id = permission_entry.identifier();
|
||||
|
||||
let permissions = get_permissions(permission_id, acl)?
|
||||
.into_iter()
|
||||
.filter(|p| p.permission.is_active(&target));
|
||||
|
||||
for TraversedPermission {
|
||||
key,
|
||||
permission_name,
|
||||
permission,
|
||||
} in permissions
|
||||
{
|
||||
let mut resolved_scope = Scopes::default();
|
||||
let mut commands = Commands::default();
|
||||
|
||||
if let PermissionEntry::ExtendedPermission {
|
||||
identifier: _,
|
||||
scope,
|
||||
} = permission_entry
|
||||
{
|
||||
if let Some(allow) = scope.allow.clone() {
|
||||
resolved_scope
|
||||
.allow
|
||||
.get_or_insert_with(Default::default)
|
||||
.extend(allow);
|
||||
}
|
||||
if let Some(deny) = scope.deny.clone() {
|
||||
resolved_scope
|
||||
.deny
|
||||
.get_or_insert_with(Default::default)
|
||||
.extend(deny);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(allow) = permission.scope.allow.clone() {
|
||||
resolved_scope
|
||||
.allow
|
||||
.get_or_insert_with(Default::default)
|
||||
.extend(allow);
|
||||
}
|
||||
if let Some(deny) = permission.scope.deny.clone() {
|
||||
resolved_scope
|
||||
.deny
|
||||
.get_or_insert_with(Default::default)
|
||||
.extend(deny);
|
||||
}
|
||||
|
||||
commands.allow.extend(permission.commands.allow.clone());
|
||||
commands.deny.extend(permission.commands.deny.clone());
|
||||
|
||||
f(ResolvedPermission {
|
||||
key: &key,
|
||||
permission_name: &permission_name,
|
||||
commands,
|
||||
scope: resolved_scope,
|
||||
})?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Traversed permission
|
||||
#[derive(Debug)]
|
||||
pub struct TraversedPermission<'a> {
|
||||
/// Plugin name without the tauri-plugin- prefix
|
||||
pub key: String,
|
||||
/// Permission's name
|
||||
pub permission_name: String,
|
||||
/// Permission details
|
||||
pub permission: &'a Permission,
|
||||
}
|
||||
|
||||
/// Expand a permissions id based on the ACL to get the associated permissions (e.g. expand some-plugin:default)
|
||||
pub fn get_permissions<'a>(
|
||||
permission_id: &Identifier,
|
||||
acl: &'a BTreeMap<String, Manifest>,
|
||||
) -> Result<Vec<TraversedPermission<'a>>, Error> {
|
||||
let key = permission_id.get_prefix().unwrap_or(APP_ACL_KEY);
|
||||
let permission_name = permission_id.get_base();
|
||||
|
||||
let manifest = acl.get(key).ok_or_else(|| Error::UnknownManifest {
|
||||
key: display_perm_key(key).to_string(),
|
||||
available: acl.keys().cloned().collect::<Vec<_>>().join(", "),
|
||||
})?;
|
||||
|
||||
if permission_name == "default" {
|
||||
manifest
|
||||
.default_permission
|
||||
.as_ref()
|
||||
.map(|default| get_permission_set_permissions(permission_id, acl, manifest, default))
|
||||
.unwrap_or_else(|| Ok(Default::default()))
|
||||
} else if let Some(set) = manifest.permission_sets.get(permission_name) {
|
||||
get_permission_set_permissions(permission_id, acl, manifest, set)
|
||||
} else if let Some(permission) = manifest.permissions.get(permission_name) {
|
||||
Ok(vec![TraversedPermission {
|
||||
key: key.to_string(),
|
||||
permission_name: permission_name.to_string(),
|
||||
permission,
|
||||
}])
|
||||
} else {
|
||||
Err(Error::UnknownPermission {
|
||||
key: display_perm_key(key).to_string(),
|
||||
permission: permission_name.to_string(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// get the permissions from a permission set
|
||||
fn get_permission_set_permissions<'a>(
|
||||
permission_id: &Identifier,
|
||||
acl: &'a BTreeMap<String, Manifest>,
|
||||
manifest: &'a Manifest,
|
||||
set: &'a PermissionSet,
|
||||
) -> Result<Vec<TraversedPermission<'a>>, Error> {
|
||||
let key = permission_id.get_prefix().unwrap_or(APP_ACL_KEY);
|
||||
|
||||
let mut permissions = Vec::new();
|
||||
|
||||
for perm in &set.permissions {
|
||||
// a set could include permissions from other plugins
|
||||
// for example `dialog:default`, could include `fs:default`
|
||||
// in this case `perm = "fs:default"` which is not a permission
|
||||
// in the dialog manifest so we check if `perm` still have a prefix (i.e `fs:`)
|
||||
// and if so, we resolve this prefix from `acl` first before proceeding
|
||||
let id = Identifier::try_from(perm.clone()).expect("invalid identifier in permission set?");
|
||||
let (manifest, permission_id, key, permission_name) =
|
||||
if let Some((new_key, manifest)) = id.get_prefix().and_then(|k| acl.get(k).map(|m| (k, m))) {
|
||||
(manifest, &id, new_key, id.get_base())
|
||||
} else {
|
||||
(manifest, permission_id, key, perm.as_str())
|
||||
};
|
||||
|
||||
if permission_name == "default" {
|
||||
permissions.extend(
|
||||
manifest
|
||||
.default_permission
|
||||
.as_ref()
|
||||
.map(|default| get_permission_set_permissions(permission_id, acl, manifest, default))
|
||||
.transpose()?
|
||||
.unwrap_or_default(),
|
||||
);
|
||||
} else if let Some(permission) = manifest.permissions.get(permission_name) {
|
||||
permissions.push(TraversedPermission {
|
||||
key: key.to_string(),
|
||||
permission_name: permission_name.to_string(),
|
||||
permission,
|
||||
});
|
||||
} else if let Some(permission_set) = manifest.permission_sets.get(permission_name) {
|
||||
permissions.extend(get_permission_set_permissions(
|
||||
permission_id,
|
||||
acl,
|
||||
manifest,
|
||||
permission_set,
|
||||
)?);
|
||||
} else {
|
||||
return Err(Error::SetPermissionNotFound {
|
||||
permission: permission_name.to_string(),
|
||||
set: set.identifier.clone(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Ok(permissions)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn display_perm_key(prefix: &str) -> &str {
|
||||
if prefix == APP_ACL_KEY {
|
||||
"app manifest"
|
||||
} else {
|
||||
prefix
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(feature = "build", feature = "build-2"))]
|
||||
mod build {
|
||||
use proc_macro2::TokenStream;
|
||||
use quote::{ToTokens, TokenStreamExt, quote};
|
||||
use std::convert::identity;
|
||||
|
||||
use super::*;
|
||||
use crate::{literal_struct, tokens::*};
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
impl ToTokens for ResolvedCommandReference {
|
||||
fn to_tokens(&self, tokens: &mut TokenStream) {
|
||||
let capability = str_lit(&self.capability);
|
||||
let permission = str_lit(&self.permission);
|
||||
literal_struct!(
|
||||
tokens,
|
||||
::tauri::utils::acl::resolved::ResolvedCommandReference,
|
||||
capability,
|
||||
permission
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl ToTokens for ResolvedCommand {
|
||||
fn to_tokens(&self, tokens: &mut TokenStream) {
|
||||
#[cfg(debug_assertions)]
|
||||
let referenced_by = &self.referenced_by;
|
||||
|
||||
let context = &self.context;
|
||||
|
||||
let windows = vec_lit(&self.windows, |window| {
|
||||
let w = window.as_str();
|
||||
quote!(#w.parse().unwrap())
|
||||
});
|
||||
let webviews = vec_lit(&self.webviews, |window| {
|
||||
let w = window.as_str();
|
||||
quote!(#w.parse().unwrap())
|
||||
});
|
||||
let scope_id = opt_lit(self.scope_id.as_ref());
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
{
|
||||
literal_struct!(
|
||||
tokens,
|
||||
::tauri::utils::acl::resolved::ResolvedCommand,
|
||||
context,
|
||||
referenced_by,
|
||||
windows,
|
||||
webviews,
|
||||
scope_id
|
||||
)
|
||||
}
|
||||
#[cfg(not(debug_assertions))]
|
||||
literal_struct!(
|
||||
tokens,
|
||||
::tauri::utils::acl::resolved::ResolvedCommand,
|
||||
context,
|
||||
windows,
|
||||
webviews,
|
||||
scope_id
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl ToTokens for ResolvedScope {
|
||||
fn to_tokens(&self, tokens: &mut TokenStream) {
|
||||
let allow = vec_lit(&self.allow, identity);
|
||||
let deny = vec_lit(&self.deny, identity);
|
||||
literal_struct!(
|
||||
tokens,
|
||||
::tauri::utils::acl::resolved::ResolvedScope,
|
||||
allow,
|
||||
deny
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl ToTokens for Resolved {
|
||||
fn to_tokens(&self, tokens: &mut TokenStream) {
|
||||
let has_app_acl = self.has_app_acl;
|
||||
|
||||
let allowed_commands = map_lit(
|
||||
quote! { ::std::collections::BTreeMap },
|
||||
&self.allowed_commands,
|
||||
str_lit,
|
||||
|v| vec_lit(v, identity),
|
||||
);
|
||||
|
||||
let denied_commands = map_lit(
|
||||
quote! { ::std::collections::BTreeMap },
|
||||
&self.denied_commands,
|
||||
str_lit,
|
||||
|v| vec_lit(v, identity),
|
||||
);
|
||||
|
||||
let command_scope = map_lit(
|
||||
quote! { ::std::collections::BTreeMap },
|
||||
&self.command_scope,
|
||||
identity,
|
||||
identity,
|
||||
);
|
||||
|
||||
let global_scope = map_lit(
|
||||
quote! { ::std::collections::BTreeMap },
|
||||
&self.global_scope,
|
||||
str_lit,
|
||||
identity,
|
||||
);
|
||||
|
||||
literal_struct!(
|
||||
tokens,
|
||||
::tauri::utils::acl::resolved::Resolved,
|
||||
has_app_acl,
|
||||
allowed_commands,
|
||||
denied_commands,
|
||||
command_scope,
|
||||
global_scope
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
|
||||
use super::{Identifier, Manifest, Permission, PermissionSet, get_permissions};
|
||||
|
||||
fn manifest<const P: usize, const S: usize>(
|
||||
name: &str,
|
||||
permissions: [&str; P],
|
||||
default_set: Option<&[&str]>,
|
||||
sets: [(&str, &[&str]); S],
|
||||
) -> (String, Manifest) {
|
||||
(
|
||||
name.to_string(),
|
||||
Manifest {
|
||||
default_permission: default_set.map(|perms| PermissionSet {
|
||||
identifier: "default".to_string(),
|
||||
description: "default set".to_string(),
|
||||
permissions: perms.iter().map(|s| s.to_string()).collect(),
|
||||
}),
|
||||
permissions: permissions
|
||||
.iter()
|
||||
.map(|p| {
|
||||
(
|
||||
p.to_string(),
|
||||
Permission {
|
||||
identifier: p.to_string(),
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
})
|
||||
.collect(),
|
||||
permission_sets: sets
|
||||
.iter()
|
||||
.map(|(s, perms)| {
|
||||
(
|
||||
s.to_string(),
|
||||
PermissionSet {
|
||||
identifier: s.to_string(),
|
||||
description: format!("{s} set"),
|
||||
permissions: perms.iter().map(|s| s.to_string()).collect(),
|
||||
},
|
||||
)
|
||||
})
|
||||
.collect(),
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
fn id(id: &str) -> Identifier {
|
||||
Identifier::try_from(id.to_string()).unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolves_permissions_from_other_plugins() {
|
||||
let acl = [
|
||||
manifest(
|
||||
"fs",
|
||||
["read", "write", "rm", "exist"],
|
||||
Some(&["read", "exist"]),
|
||||
[],
|
||||
),
|
||||
manifest(
|
||||
"http",
|
||||
["fetch", "fetch-cancel"],
|
||||
None,
|
||||
[("fetch-with-cancel", &["fetch", "fetch-cancel"])],
|
||||
),
|
||||
manifest(
|
||||
"dialog",
|
||||
["open", "save"],
|
||||
None,
|
||||
[(
|
||||
"extra",
|
||||
&[
|
||||
"save",
|
||||
"fs:default",
|
||||
"fs:write",
|
||||
"http:default",
|
||||
"http:fetch-with-cancel",
|
||||
],
|
||||
)],
|
||||
),
|
||||
]
|
||||
.into();
|
||||
|
||||
let permissions = get_permissions(&id("fs:default"), &acl).unwrap();
|
||||
assert_eq!(permissions.len(), 2);
|
||||
assert_eq!(permissions[0].key, "fs");
|
||||
assert_eq!(permissions[0].permission_name, "read");
|
||||
assert_eq!(permissions[1].key, "fs");
|
||||
assert_eq!(permissions[1].permission_name, "exist");
|
||||
|
||||
let permissions = get_permissions(&id("fs:rm"), &acl).unwrap();
|
||||
assert_eq!(permissions.len(), 1);
|
||||
assert_eq!(permissions[0].key, "fs");
|
||||
assert_eq!(permissions[0].permission_name, "rm");
|
||||
|
||||
let permissions = get_permissions(&id("http:fetch-with-cancel"), &acl).unwrap();
|
||||
assert_eq!(permissions.len(), 2);
|
||||
assert_eq!(permissions[0].key, "http");
|
||||
assert_eq!(permissions[0].permission_name, "fetch");
|
||||
assert_eq!(permissions[1].key, "http");
|
||||
assert_eq!(permissions[1].permission_name, "fetch-cancel");
|
||||
|
||||
let permissions = get_permissions(&id("dialog:extra"), &acl).unwrap();
|
||||
assert_eq!(permissions.len(), 6);
|
||||
assert_eq!(permissions[0].key, "dialog");
|
||||
assert_eq!(permissions[0].permission_name, "save");
|
||||
assert_eq!(permissions[1].key, "fs");
|
||||
assert_eq!(permissions[1].permission_name, "read");
|
||||
assert_eq!(permissions[2].key, "fs");
|
||||
assert_eq!(permissions[2].permission_name, "exist");
|
||||
assert_eq!(permissions[3].key, "fs");
|
||||
assert_eq!(permissions[3].permission_name, "write");
|
||||
assert_eq!(permissions[4].key, "http");
|
||||
assert_eq!(permissions[4].permission_name, "fetch");
|
||||
assert_eq!(permissions[5].key, "http");
|
||||
assert_eq!(permissions[5].permission_name, "fetch-cancel");
|
||||
}
|
||||
}
|
||||
445
apps/tauri/src-tauri/vendor/tauri-utils/src/acl/schema.rs
vendored
Normal file
445
apps/tauri/src-tauri/vendor/tauri-utils/src/acl/schema.rs
vendored
Normal file
|
|
@ -0,0 +1,445 @@
|
|||
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
//! Schema generation for ACL items.
|
||||
|
||||
use std::{
|
||||
collections::{BTreeMap, btree_map::Values},
|
||||
fs,
|
||||
path::{Path, PathBuf},
|
||||
slice::Iter,
|
||||
};
|
||||
|
||||
use schemars::Schema;
|
||||
|
||||
use super::{Error, PERMISSION_SCHEMAS_FOLDER_NAME};
|
||||
use crate::{platform::Target, write_if_changed};
|
||||
|
||||
use super::{
|
||||
PERMISSION_SCHEMA_FILE_NAME, Permission, PermissionSet,
|
||||
capability::CapabilityFile,
|
||||
manifest::{Manifest, PermissionFile},
|
||||
};
|
||||
|
||||
/// Capability schema file name.
|
||||
pub const CAPABILITIES_SCHEMA_FILE_NAME: &str = "schema.json";
|
||||
/// Path of the folder where schemas are saved.
|
||||
pub const CAPABILITIES_SCHEMA_FOLDER_PATH: &str = "gen/schemas";
|
||||
|
||||
// TODO: once MSRV is high enough, remove generic and use impl <trait>
|
||||
// see https://github.com/tauri-apps/tauri/commit/b5561d74aee431f93c0c5b0fa6784fc0a956effe#diff-7c31d393f83cae149122e74ad44ac98e7d70ffb45c9e5b0a94ec52881b6f1cebR30-R42
|
||||
/// Permission schema generator trait
|
||||
pub trait PermissionSchemaGenerator<
|
||||
'a,
|
||||
Ps: Iterator<Item = &'a PermissionSet>,
|
||||
P: Iterator<Item = &'a Permission>,
|
||||
>
|
||||
{
|
||||
/// Whether has a default permission set or not.
|
||||
fn has_default_permission_set(&self) -> bool;
|
||||
|
||||
/// Default permission set description if any.
|
||||
fn default_set_description(&self) -> Option<&str>;
|
||||
|
||||
/// Default permission set's permissions if any.
|
||||
fn default_set_permissions(&self) -> Option<&Vec<String>>;
|
||||
|
||||
/// Permissions sets to generate schema for.
|
||||
fn permission_sets(&'a self) -> Ps;
|
||||
|
||||
/// Permissions to generate schema for.
|
||||
fn permissions(&'a self) -> P;
|
||||
|
||||
/// A utility function to generate a schema for a permission identifier
|
||||
fn perm_id_schema(name: Option<&str>, id: &str, description: Option<&str>) -> Schema {
|
||||
let command_name = match name {
|
||||
Some(name) if name == super::APP_ACL_KEY => id.to_string(),
|
||||
Some(name) => format!("{name}:{id}"),
|
||||
_ => id.to_string(),
|
||||
};
|
||||
|
||||
let mut schema = schemars::json_schema!({
|
||||
"type": "string",
|
||||
"const": command_name
|
||||
});
|
||||
|
||||
if let Some(description) = description {
|
||||
schema.insert(
|
||||
"description".to_string(),
|
||||
serde_json::Value::String(description.to_string()),
|
||||
);
|
||||
// Non-standard, used by vscode for rich hover tooltips
|
||||
schema.insert(
|
||||
"markdownDescription".to_string(),
|
||||
serde_json::Value::String(description.to_string()),
|
||||
);
|
||||
}
|
||||
|
||||
schema
|
||||
}
|
||||
|
||||
/// Generate schemas for all possible permissions.
|
||||
fn gen_possible_permission_schemas(&'a self, name: Option<&str>) -> Vec<Schema> {
|
||||
let mut permission_schemas = Vec::new();
|
||||
|
||||
// schema for default set
|
||||
if self.has_default_permission_set() {
|
||||
let description = self.default_set_description().unwrap_or_default();
|
||||
let description = if let Some(permissions) = self.default_set_permissions() {
|
||||
add_permissions_to_description(description, permissions, true)
|
||||
} else {
|
||||
description.to_string()
|
||||
};
|
||||
if !description.is_empty() {
|
||||
let default = Self::perm_id_schema(name, "default", Some(&description));
|
||||
permission_schemas.push(default);
|
||||
}
|
||||
}
|
||||
|
||||
// schema for each permission set
|
||||
for set in self.permission_sets() {
|
||||
let description = add_permissions_to_description(&set.description, &set.permissions, false);
|
||||
let schema = Self::perm_id_schema(name, &set.identifier, Some(&description));
|
||||
permission_schemas.push(schema);
|
||||
}
|
||||
|
||||
// schema for each permission
|
||||
for perm in self.permissions() {
|
||||
let schema = Self::perm_id_schema(name, &perm.identifier, perm.description.as_deref());
|
||||
permission_schemas.push(schema);
|
||||
}
|
||||
|
||||
permission_schemas
|
||||
}
|
||||
}
|
||||
|
||||
fn add_permissions_to_description(
|
||||
description: &str,
|
||||
permissions: &[String],
|
||||
is_default: bool,
|
||||
) -> String {
|
||||
if permissions.is_empty() {
|
||||
return description.to_string();
|
||||
}
|
||||
let permissions_list = permissions
|
||||
.iter()
|
||||
.map(|permission| format!("- `{permission}`"))
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n");
|
||||
let default_permission_set = if is_default {
|
||||
"default permission set"
|
||||
} else {
|
||||
"permission set"
|
||||
};
|
||||
format!("{description}\n#### This {default_permission_set} includes:\n\n{permissions_list}")
|
||||
}
|
||||
|
||||
impl<'a>
|
||||
PermissionSchemaGenerator<
|
||||
'a,
|
||||
Values<'a, std::string::String, PermissionSet>,
|
||||
Values<'a, std::string::String, Permission>,
|
||||
> for Manifest
|
||||
{
|
||||
fn has_default_permission_set(&self) -> bool {
|
||||
self.default_permission.is_some()
|
||||
}
|
||||
|
||||
fn default_set_description(&self) -> Option<&str> {
|
||||
self
|
||||
.default_permission
|
||||
.as_ref()
|
||||
.map(|d| d.description.as_str())
|
||||
}
|
||||
|
||||
fn default_set_permissions(&self) -> Option<&Vec<String>> {
|
||||
self.default_permission.as_ref().map(|d| &d.permissions)
|
||||
}
|
||||
|
||||
fn permission_sets(&'a self) -> Values<'a, std::string::String, PermissionSet> {
|
||||
self.permission_sets.values()
|
||||
}
|
||||
|
||||
fn permissions(&'a self) -> Values<'a, std::string::String, Permission> {
|
||||
self.permissions.values()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> PermissionSchemaGenerator<'a, Iter<'a, PermissionSet>, Iter<'a, Permission>>
|
||||
for PermissionFile
|
||||
{
|
||||
fn has_default_permission_set(&self) -> bool {
|
||||
self.default.is_some()
|
||||
}
|
||||
|
||||
fn default_set_description(&self) -> Option<&str> {
|
||||
self.default.as_ref().and_then(|d| d.description.as_deref())
|
||||
}
|
||||
|
||||
fn default_set_permissions(&self) -> Option<&Vec<String>> {
|
||||
self.default.as_ref().map(|d| &d.permissions)
|
||||
}
|
||||
|
||||
fn permission_sets(&'a self) -> Iter<'a, PermissionSet> {
|
||||
self.set.iter()
|
||||
}
|
||||
|
||||
fn permissions(&'a self) -> Iter<'a, Permission> {
|
||||
self.permission.iter()
|
||||
}
|
||||
}
|
||||
|
||||
/// Collect and include all possible identifiers in `Identifier` definition in the schema
|
||||
fn extend_identifier_schema(schema: &mut Schema, acl: &BTreeMap<String, Manifest>) {
|
||||
let permission_schemas: Vec<serde_json::Value> = acl
|
||||
.iter()
|
||||
.flat_map(|(name, manifest)| manifest.gen_possible_permission_schemas(Some(name)))
|
||||
.map(serde_json::Value::from)
|
||||
.collect();
|
||||
|
||||
if let Some(identifier_schema) = schema
|
||||
.pointer_mut("/$defs/Identifier")
|
||||
.and_then(|v| v.as_object_mut())
|
||||
{
|
||||
identifier_schema.insert(
|
||||
"oneOf".to_string(),
|
||||
serde_json::Value::Array(permission_schemas),
|
||||
);
|
||||
identifier_schema.remove("properties");
|
||||
identifier_schema.remove("type");
|
||||
identifier_schema.insert(
|
||||
"description".to_string(),
|
||||
serde_json::Value::String("Permission identifier".to_string()),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Collect permission schemas and its associated scope schema and schema definitions from plugins
|
||||
/// and replace `PermissionEntry` extend object syntax with a new schema that does conditional
|
||||
/// checks to serve the relevant scope schema for the right permissions schema, in a nutshell, it
|
||||
/// will look something like this:
|
||||
/// ```text
|
||||
/// PermissionEntry {
|
||||
/// anyOf {
|
||||
/// String, // default string syntax
|
||||
/// Object { // extended object syntax
|
||||
/// allOf { // JSON allOf is used but actually means anyOf
|
||||
/// {
|
||||
/// "if": "identifier" property anyOf "fs" plugin permission,
|
||||
/// "then": add "allow" and "deny" properties that match "fs" plugin scope schema
|
||||
/// },
|
||||
/// {
|
||||
/// "if": "identifier" property anyOf "http" plugin permission,
|
||||
/// "then": add "allow" and "deny" properties that match "http" plugin scope schema
|
||||
/// },
|
||||
/// ...etc,
|
||||
/// {
|
||||
/// No "if" or "then", just "allow" and "deny" properties with default "#/defs/Value"
|
||||
/// },
|
||||
/// }
|
||||
/// }
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
fn extend_permission_entry_schema(root_schema: &mut Schema, acl: &BTreeMap<String, Manifest>) {
|
||||
const IDENTIFIER: &str = "identifier";
|
||||
const ALLOW: &str = "allow";
|
||||
const DENY: &str = "deny";
|
||||
|
||||
let mut collected_defs: serde_json::Map<String, serde_json::Value> = serde_json::Map::new();
|
||||
|
||||
// Scope the mutable borrow of root_schema
|
||||
{
|
||||
let defs = match root_schema.get_mut("$defs").and_then(|v| v.as_object_mut()) {
|
||||
Some(d) => d,
|
||||
None => return,
|
||||
};
|
||||
|
||||
let perm_entry = match defs
|
||||
.get_mut("PermissionEntry")
|
||||
.and_then(|v| v.as_object_mut())
|
||||
{
|
||||
Some(p) => p,
|
||||
None => return,
|
||||
};
|
||||
|
||||
let any_of = match perm_entry.get_mut("anyOf").and_then(|v| v.as_array_mut()) {
|
||||
Some(a) => a,
|
||||
None => return,
|
||||
};
|
||||
|
||||
let extend_perm_entry = match any_of.last_mut().and_then(|v| v.as_object_mut()) {
|
||||
Some(e) => e,
|
||||
None => return,
|
||||
};
|
||||
|
||||
// Remove default properties and save to be added later as a fallback
|
||||
let default_properties = extend_perm_entry
|
||||
.remove("properties")
|
||||
.and_then(|v| match v {
|
||||
serde_json::Value::Object(m) => Some(m),
|
||||
_ => None,
|
||||
})
|
||||
.unwrap_or_default();
|
||||
|
||||
let default_identifier = default_properties.get(IDENTIFIER).cloned().unwrap();
|
||||
|
||||
let mut all_of: Vec<serde_json::Value> = vec![];
|
||||
|
||||
let schemas = acl.iter().filter_map(|(name, manifest)| {
|
||||
manifest
|
||||
.global_scope_schema()
|
||||
.unwrap_or_else(|e| panic!("invalid JSON schema for plugin {name}: {e}"))
|
||||
.map(|s| (s, manifest.gen_possible_permission_schemas(Some(name))))
|
||||
});
|
||||
|
||||
for ((scope_schema, defs), acl_perm_schema) in schemas {
|
||||
let perm_schema_values: Vec<serde_json::Value> = acl_perm_schema
|
||||
.into_iter()
|
||||
.map(serde_json::Value::from)
|
||||
.collect();
|
||||
|
||||
let scope_value = serde_json::Value::from(scope_schema);
|
||||
|
||||
let obj = serde_json::json!({
|
||||
"properties": {
|
||||
IDENTIFIER: default_identifier.clone()
|
||||
},
|
||||
"if": {
|
||||
"properties": {
|
||||
IDENTIFIER: { "anyOf": perm_schema_values }
|
||||
}
|
||||
},
|
||||
"then": {
|
||||
"properties": {
|
||||
ALLOW: scope_value.clone(),
|
||||
DENY: scope_value,
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
all_of.push(obj);
|
||||
collected_defs.extend(defs);
|
||||
}
|
||||
|
||||
// Add back default properties as a fallback
|
||||
all_of.push(serde_json::json!({
|
||||
"properties": serde_json::Value::Object(default_properties)
|
||||
}));
|
||||
|
||||
// Replace extended PermissionEntry with the new schema
|
||||
extend_perm_entry.insert("allOf".to_string(), serde_json::Value::Array(all_of));
|
||||
}
|
||||
|
||||
// Extend root schema with definitions collected from plugins
|
||||
if !collected_defs.is_empty() {
|
||||
let root_defs = root_schema
|
||||
.ensure_object()
|
||||
.entry("$defs")
|
||||
.or_insert(serde_json::Value::Object(serde_json::Map::new()))
|
||||
.as_object_mut()
|
||||
.unwrap();
|
||||
|
||||
root_defs.extend(collected_defs);
|
||||
}
|
||||
}
|
||||
|
||||
/// Generate schema for CapabilityFile with all possible plugins permissions
|
||||
pub fn generate_capability_schema(
|
||||
acl: &BTreeMap<String, Manifest>,
|
||||
target: Target,
|
||||
) -> crate::Result<()> {
|
||||
let mut schema = schemars::SchemaGenerator::new(schemars::generate::SchemaSettings::draft07())
|
||||
.into_root_schema_for::<CapabilityFile>();
|
||||
|
||||
extend_identifier_schema(&mut schema, acl);
|
||||
extend_permission_entry_schema(&mut schema, acl);
|
||||
|
||||
let schema_str = serde_json::to_string_pretty(&schema).unwrap();
|
||||
|
||||
let out_dir = PathBuf::from(CAPABILITIES_SCHEMA_FOLDER_PATH);
|
||||
fs::create_dir_all(&out_dir)?;
|
||||
|
||||
let schema_path = out_dir.join(format!("{target}-{CAPABILITIES_SCHEMA_FILE_NAME}"));
|
||||
if schema_str != fs::read_to_string(&schema_path).unwrap_or_default() {
|
||||
fs::write(&schema_path, schema_str)?;
|
||||
|
||||
fs::copy(
|
||||
schema_path,
|
||||
out_dir.join(format!(
|
||||
"{}-{CAPABILITIES_SCHEMA_FILE_NAME}",
|
||||
if target.is_desktop() {
|
||||
"desktop"
|
||||
} else {
|
||||
"mobile"
|
||||
}
|
||||
)),
|
||||
)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Extend schema with collected permissions from the passed [`PermissionFile`]s.
|
||||
fn extend_permission_file_schema(schema: &mut Schema, permissions: &[PermissionFile]) {
|
||||
// Collect possible permissions
|
||||
let permission_schemas: Vec<serde_json::Value> = permissions
|
||||
.iter()
|
||||
.flat_map(|p| p.gen_possible_permission_schemas(None))
|
||||
.map(serde_json::Value::from)
|
||||
.collect();
|
||||
|
||||
// Update the permissions property to reference PermissionKind
|
||||
let updated = if let Some(permissions_obj) = schema
|
||||
.pointer_mut("/$defs/PermissionSet/properties/permissions")
|
||||
.and_then(|v| v.as_object_mut())
|
||||
{
|
||||
permissions_obj.insert(
|
||||
"items".to_string(),
|
||||
serde_json::json!({ "$ref": "#/$defs/PermissionKind" }),
|
||||
);
|
||||
true
|
||||
} else {
|
||||
false
|
||||
};
|
||||
|
||||
// Add the new PermissionKind definition
|
||||
if updated {
|
||||
let defs = schema
|
||||
.ensure_object()
|
||||
.entry("$defs")
|
||||
.or_insert(serde_json::Value::Object(serde_json::Map::new()))
|
||||
.as_object_mut()
|
||||
.unwrap();
|
||||
|
||||
defs.insert(
|
||||
"PermissionKind".into(),
|
||||
serde_json::json!({
|
||||
"type": "string",
|
||||
"oneOf": permission_schemas,
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Generate and write a schema based on the format of a [`PermissionFile`].
|
||||
pub fn generate_permissions_schema<P: AsRef<Path>>(
|
||||
permissions: &[PermissionFile],
|
||||
out_dir: P,
|
||||
) -> Result<(), Error> {
|
||||
let mut schema = schemars::SchemaGenerator::new(schemars::generate::SchemaSettings::draft07())
|
||||
.into_root_schema_for::<PermissionFile>();
|
||||
|
||||
extend_permission_file_schema(&mut schema, permissions);
|
||||
|
||||
let schema_str = serde_json::to_string_pretty(&schema)?;
|
||||
|
||||
let out_dir = out_dir.as_ref().join(PERMISSION_SCHEMAS_FOLDER_NAME);
|
||||
fs::create_dir_all(&out_dir).map_err(|e| Error::CreateDir(e, out_dir.clone()))?;
|
||||
|
||||
let schema_path = out_dir.join(PERMISSION_SCHEMA_FILE_NAME);
|
||||
write_if_changed(&schema_path, schema_str).map_err(|e| Error::WriteFile(e, schema_path))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
201
apps/tauri/src-tauri/vendor/tauri-utils/src/acl/value.rs
vendored
Normal file
201
apps/tauri/src-tauri/vendor/tauri-utils/src/acl/value.rs
vendored
Normal file
|
|
@ -0,0 +1,201 @@
|
|||
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
//! A [`Value`] that is used instead of [`toml::Value`] or [`serde_json::Value`]
|
||||
//! to support both formats.
|
||||
|
||||
use std::collections::BTreeMap;
|
||||
use std::fmt::Debug;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// A valid ACL number.
|
||||
#[derive(Debug, PartialEq, Serialize, Deserialize, Copy, Clone, PartialOrd)]
|
||||
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
|
||||
#[serde(untagged)]
|
||||
pub enum Number {
|
||||
/// Represents an [`i64`].
|
||||
Int(i64),
|
||||
|
||||
/// Represents a [`f64`].
|
||||
Float(f64),
|
||||
}
|
||||
|
||||
impl From<i64> for Number {
|
||||
#[inline(always)]
|
||||
fn from(value: i64) -> Self {
|
||||
Self::Int(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<f64> for Number {
|
||||
#[inline(always)]
|
||||
fn from(value: f64) -> Self {
|
||||
Self::Float(value)
|
||||
}
|
||||
}
|
||||
|
||||
/// All supported ACL values.
|
||||
#[derive(Debug, PartialEq, Serialize, Deserialize, Clone, PartialOrd)]
|
||||
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
|
||||
#[serde(untagged)]
|
||||
pub enum Value {
|
||||
/// Represents a null JSON value.
|
||||
Null,
|
||||
|
||||
/// Represents a [`bool`].
|
||||
Bool(bool),
|
||||
|
||||
/// Represents a valid ACL [`Number`].
|
||||
Number(Number),
|
||||
|
||||
/// Represents a [`String`].
|
||||
String(String),
|
||||
|
||||
/// Represents a list of other [`Value`]s.
|
||||
List(Vec<Value>),
|
||||
|
||||
/// Represents a map of [`String`] keys to [`Value`]s.
|
||||
Map(BTreeMap<String, Value>),
|
||||
}
|
||||
|
||||
impl From<Value> for serde_json::Value {
|
||||
fn from(value: Value) -> Self {
|
||||
match value {
|
||||
Value::Null => serde_json::Value::Null,
|
||||
Value::Bool(b) => serde_json::Value::Bool(b),
|
||||
Value::Number(Number::Float(f)) => {
|
||||
serde_json::Value::Number(serde_json::Number::from_f64(f).unwrap())
|
||||
}
|
||||
Value::Number(Number::Int(i)) => serde_json::Value::Number(i.into()),
|
||||
Value::String(s) => serde_json::Value::String(s),
|
||||
Value::List(list) => serde_json::Value::Array(list.into_iter().map(Into::into).collect()),
|
||||
Value::Map(map) => serde_json::Value::Object(
|
||||
map
|
||||
.into_iter()
|
||||
.map(|(key, value)| (key, value.into()))
|
||||
.collect(),
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<serde_json::Value> for Value {
|
||||
fn from(value: serde_json::Value) -> Self {
|
||||
match value {
|
||||
serde_json::Value::Null => Value::Null,
|
||||
serde_json::Value::Bool(b) => Value::Bool(b),
|
||||
serde_json::Value::Number(n) => Value::Number(if let Some(f) = n.as_f64() {
|
||||
Number::Float(f)
|
||||
} else if let Some(n) = n.as_u64() {
|
||||
Number::Int(n as i64)
|
||||
} else if let Some(n) = n.as_i64() {
|
||||
Number::Int(n)
|
||||
} else {
|
||||
Number::Int(0)
|
||||
}),
|
||||
serde_json::Value::String(s) => Value::String(s),
|
||||
serde_json::Value::Array(list) => Value::List(list.into_iter().map(Into::into).collect()),
|
||||
serde_json::Value::Object(map) => Value::Map(
|
||||
map
|
||||
.into_iter()
|
||||
.map(|(key, value)| (key, value.into()))
|
||||
.collect(),
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<bool> for Value {
|
||||
#[inline(always)]
|
||||
fn from(value: bool) -> Self {
|
||||
Self::Bool(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Into<Number>> From<T> for Value {
|
||||
#[inline(always)]
|
||||
fn from(value: T) -> Self {
|
||||
Self::Number(value.into())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<String> for Value {
|
||||
#[inline(always)]
|
||||
fn from(value: String) -> Self {
|
||||
Value::String(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<toml::Value> for Value {
|
||||
#[inline(always)]
|
||||
fn from(value: toml::Value) -> Self {
|
||||
use toml::Value as Toml;
|
||||
|
||||
match value {
|
||||
Toml::String(s) => s.into(),
|
||||
Toml::Integer(i) => i.into(),
|
||||
Toml::Float(f) => f.into(),
|
||||
Toml::Boolean(b) => b.into(),
|
||||
Toml::Datetime(d) => d.to_string().into(),
|
||||
Toml::Array(a) => Value::List(a.into_iter().map(Value::from).collect()),
|
||||
Toml::Table(t) => Value::Map(t.into_iter().map(|(k, v)| (k, v.into())).collect()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(feature = "build", feature = "build-2"))]
|
||||
mod build {
|
||||
use std::convert::identity;
|
||||
|
||||
use crate::tokens::*;
|
||||
|
||||
use super::*;
|
||||
use proc_macro2::TokenStream;
|
||||
use quote::{ToTokens, TokenStreamExt, quote};
|
||||
|
||||
impl ToTokens for Number {
|
||||
fn to_tokens(&self, tokens: &mut TokenStream) {
|
||||
let prefix = quote! { ::tauri::utils::acl::Number };
|
||||
|
||||
tokens.append_all(match self {
|
||||
Self::Int(i) => {
|
||||
quote! { #prefix::Int(#i) }
|
||||
}
|
||||
Self::Float(f) => {
|
||||
quote! { #prefix::Float (#f) }
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl ToTokens for Value {
|
||||
fn to_tokens(&self, tokens: &mut TokenStream) {
|
||||
let prefix = quote! { ::tauri::utils::acl::Value };
|
||||
|
||||
tokens.append_all(match self {
|
||||
Value::Null => quote! { #prefix::Null },
|
||||
Value::Bool(bool) => quote! { #prefix::Bool(#bool) },
|
||||
Value::Number(number) => quote! { #prefix::Number(#number) },
|
||||
Value::String(str) => {
|
||||
let s = str_lit(str);
|
||||
quote! { #prefix::String(#s) }
|
||||
}
|
||||
Value::List(vec) => {
|
||||
let items = vec_lit(vec, identity);
|
||||
quote! { #prefix::List(#items) }
|
||||
}
|
||||
Value::Map(map) => {
|
||||
let map = map_lit(
|
||||
quote! { ::std::collections::BTreeMap },
|
||||
map,
|
||||
str_lit,
|
||||
identity,
|
||||
);
|
||||
quote! { #prefix::Map(#map) }
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
212
apps/tauri/src-tauri/vendor/tauri-utils/src/assets.rs
vendored
Normal file
212
apps/tauri/src-tauri/vendor/tauri-utils/src/assets.rs
vendored
Normal file
|
|
@ -0,0 +1,212 @@
|
|||
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
//! The Assets module allows you to read files that have been bundled by tauri
|
||||
//! during both compile time and runtime.
|
||||
|
||||
#[doc(hidden)]
|
||||
pub use phf;
|
||||
use std::{
|
||||
borrow::Cow,
|
||||
path::{Component, Path},
|
||||
};
|
||||
|
||||
/// The token used for script nonces.
|
||||
pub const SCRIPT_NONCE_TOKEN: &str = "__TAURI_SCRIPT_NONCE__";
|
||||
/// The token used for style nonces.
|
||||
pub const STYLE_NONCE_TOKEN: &str = "__TAURI_STYLE_NONCE__";
|
||||
|
||||
/// Assets iterator.
|
||||
pub type AssetsIter<'a> = dyn Iterator<Item = (Cow<'a, str>, Cow<'a, [u8]>)> + 'a;
|
||||
|
||||
/// Represent an asset file path in a normalized way.
|
||||
///
|
||||
/// The following rules are enforced and added if needed:
|
||||
/// * Unix path component separators
|
||||
/// * Has a root directory
|
||||
/// * No trailing slash - directories are not included in assets
|
||||
#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
|
||||
pub struct AssetKey(String);
|
||||
|
||||
impl From<AssetKey> for String {
|
||||
fn from(key: AssetKey) -> Self {
|
||||
key.0
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<str> for AssetKey {
|
||||
fn as_ref(&self) -> &str {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl<P: AsRef<Path>> From<P> for AssetKey {
|
||||
fn from(path: P) -> Self {
|
||||
// TODO: change this to utilize `Cow` to prevent allocating an intermediate `PathBuf` when not necessary
|
||||
let path = path.as_ref().to_owned();
|
||||
|
||||
// add in root to mimic how it is used from a server url
|
||||
let path = if path.has_root() {
|
||||
path
|
||||
} else {
|
||||
Path::new(&Component::RootDir).join(path)
|
||||
};
|
||||
|
||||
let buf = if cfg!(windows) {
|
||||
let mut buf = String::new();
|
||||
for component in path.components() {
|
||||
match component {
|
||||
Component::RootDir => buf.push('/'),
|
||||
Component::CurDir => buf.push_str("./"),
|
||||
Component::ParentDir => buf.push_str("../"),
|
||||
Component::Prefix(prefix) => buf.push_str(&prefix.as_os_str().to_string_lossy()),
|
||||
Component::Normal(s) => {
|
||||
buf.push_str(&s.to_string_lossy());
|
||||
buf.push('/')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// remove the last slash
|
||||
if buf != "/" {
|
||||
buf.pop();
|
||||
}
|
||||
|
||||
buf
|
||||
} else {
|
||||
path.to_string_lossy().to_string()
|
||||
};
|
||||
|
||||
AssetKey(buf)
|
||||
}
|
||||
}
|
||||
|
||||
/// A Content-Security-Policy hash value for a specific directive.
|
||||
/// For more information see [the MDN page](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy#directives).
|
||||
#[non_exhaustive]
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum CspHash<'a> {
|
||||
/// The `script-src` directive.
|
||||
Script(&'a str),
|
||||
|
||||
/// The `style-src` directive.
|
||||
Style(&'a str),
|
||||
}
|
||||
|
||||
impl CspHash<'_> {
|
||||
/// The Content-Security-Policy directive this hash applies to.
|
||||
pub fn directive(&self) -> &'static str {
|
||||
match self {
|
||||
Self::Script(_) => "script-src",
|
||||
Self::Style(_) => "style-src",
|
||||
}
|
||||
}
|
||||
|
||||
/// The value of the Content-Security-Policy hash.
|
||||
pub fn hash(&self) -> &str {
|
||||
match self {
|
||||
Self::Script(hash) => hash,
|
||||
Self::Style(hash) => hash,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// [`Assets`] implementation that only contains compile-time compressed and embedded assets.
|
||||
pub struct EmbeddedAssets {
|
||||
assets: phf::Map<&'static str, &'static [u8]>,
|
||||
// Hashes that must be injected to the CSP of every HTML file.
|
||||
global_hashes: &'static [CspHash<'static>],
|
||||
// Hashes that are associated to the CSP of the HTML file identified by the map key (the HTML asset key).
|
||||
html_hashes: phf::Map<&'static str, &'static [CspHash<'static>]>,
|
||||
}
|
||||
|
||||
/// Temporary struct that overrides the Debug formatting for the `assets` field.
|
||||
///
|
||||
/// It reduces the output size compared to the default, as that would format the binary
|
||||
/// data as a slice of numbers like `[65, 66, 67]` for "ABC". This instead shows the length
|
||||
/// of the slice.
|
||||
///
|
||||
/// For example: `{"/index.html": [u8; 1835], "/index.js": [u8; 212]}`
|
||||
struct DebugAssetMap<'a>(&'a phf::Map<&'static str, &'static [u8]>);
|
||||
|
||||
impl std::fmt::Debug for DebugAssetMap<'_> {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
let mut map = f.debug_map();
|
||||
for (k, v) in self.0.entries() {
|
||||
map.key(k);
|
||||
map.value(&format_args!("[u8; {}]", v.len()));
|
||||
}
|
||||
map.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for EmbeddedAssets {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("EmbeddedAssets")
|
||||
.field("assets", &DebugAssetMap(&self.assets))
|
||||
.field("global_hashes", &self.global_hashes)
|
||||
.field("html_hashes", &self.html_hashes)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl EmbeddedAssets {
|
||||
/// Creates a new instance from the given asset map and script hash list.
|
||||
pub const fn new(
|
||||
map: phf::Map<&'static str, &'static [u8]>,
|
||||
global_hashes: &'static [CspHash<'static>],
|
||||
html_hashes: phf::Map<&'static str, &'static [CspHash<'static>]>,
|
||||
) -> Self {
|
||||
Self {
|
||||
assets: map,
|
||||
global_hashes,
|
||||
html_hashes,
|
||||
}
|
||||
}
|
||||
|
||||
/// Get an asset by key.
|
||||
#[cfg(feature = "compression")]
|
||||
pub fn get(&self, key: &AssetKey) -> Option<Cow<'_, [u8]>> {
|
||||
let &(mut asdf) = self.assets.get(key.as_ref())?;
|
||||
// with the exception of extremely small files, output should usually be
|
||||
// at least as large as the compressed version.
|
||||
let mut buf = Vec::with_capacity(asdf.len());
|
||||
brotli::BrotliDecompress(&mut asdf, &mut buf).ok()?;
|
||||
Some(Cow::Owned(buf))
|
||||
}
|
||||
|
||||
/// Get an asset by key.
|
||||
#[cfg(not(feature = "compression"))]
|
||||
pub fn get(&self, key: &AssetKey) -> Option<Cow<'_, [u8]>> {
|
||||
Some(Cow::Borrowed(self.assets.get(key.as_ref())?))
|
||||
}
|
||||
|
||||
/// Iterate on the assets.
|
||||
pub fn iter(&self) -> Box<AssetsIter<'_>> {
|
||||
Box::new(
|
||||
self
|
||||
.assets
|
||||
.into_iter()
|
||||
.map(|(k, b)| (Cow::Borrowed(*k), Cow::Borrowed(*b))),
|
||||
)
|
||||
}
|
||||
|
||||
/// CSP hashes for the given asset.
|
||||
pub fn csp_hashes(&self, html_path: &AssetKey) -> Box<dyn Iterator<Item = CspHash<'_>> + '_> {
|
||||
Box::new(
|
||||
self
|
||||
.global_hashes
|
||||
.iter()
|
||||
.chain(
|
||||
self
|
||||
.html_hashes
|
||||
.get(html_path.as_ref())
|
||||
.copied()
|
||||
.into_iter()
|
||||
.flatten(),
|
||||
)
|
||||
.copied(),
|
||||
)
|
||||
}
|
||||
}
|
||||
171
apps/tauri/src-tauri/vendor/tauri-utils/src/build.rs
vendored
Normal file
171
apps/tauri/src-tauri/vendor/tauri-utils/src/build.rs
vendored
Normal file
|
|
@ -0,0 +1,171 @@
|
|||
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
//! Build script utilities.
|
||||
|
||||
/// Link a Swift library.
|
||||
#[cfg(target_os = "macos")]
|
||||
pub fn link_apple_library(name: &str, source: impl AsRef<std::path::Path>) {
|
||||
if source.as_ref().join("Package.swift").exists() {
|
||||
link_swift_library(name, source);
|
||||
} else {
|
||||
link_xcode_library(name, source);
|
||||
}
|
||||
}
|
||||
|
||||
/// Link a Swift library.
|
||||
#[cfg(target_os = "macos")]
|
||||
fn link_swift_library(name: &str, source: impl AsRef<std::path::Path>) {
|
||||
let source = source.as_ref();
|
||||
|
||||
let sdk_root = std::env::var_os("SDKROOT");
|
||||
unsafe {
|
||||
std::env::remove_var("SDKROOT");
|
||||
}
|
||||
|
||||
swift_rs::SwiftLinker::new(
|
||||
&std::env::var("MACOSX_DEPLOYMENT_TARGET").unwrap_or_else(|_| "10.13".into()),
|
||||
)
|
||||
.with_ios(&std::env::var("IPHONEOS_DEPLOYMENT_TARGET").unwrap_or_else(|_| "13.0".into()))
|
||||
.with_package(name, source)
|
||||
.link();
|
||||
|
||||
if let Some(root) = sdk_root {
|
||||
unsafe {
|
||||
std::env::set_var("SDKROOT", root);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Link a Xcode library.
|
||||
#[cfg(target_os = "macos")]
|
||||
fn link_xcode_library(name: &str, source: impl AsRef<std::path::Path>) {
|
||||
use std::{path::PathBuf, process::Command};
|
||||
|
||||
let source = source.as_ref();
|
||||
let configuration = if std::env::var("DEBUG")
|
||||
.map(|v| v == "true")
|
||||
.unwrap_or_default()
|
||||
{
|
||||
"Debug"
|
||||
} else {
|
||||
"Release"
|
||||
};
|
||||
|
||||
let (sdk, arch) = match std::env::var("TARGET").unwrap().as_str() {
|
||||
"aarch64-apple-ios" => ("iphoneos", "arm64"),
|
||||
"aarch64-apple-ios-sim" => ("iphonesimulator", "arm64"),
|
||||
"x86_64-apple-ios" => ("iphonesimulator", "x86_64"),
|
||||
_ => return,
|
||||
};
|
||||
|
||||
let out_dir = std::env::var_os("OUT_DIR").map(PathBuf::from).unwrap();
|
||||
let derived_data_path = out_dir.join(format!("derivedData-{name}"));
|
||||
|
||||
let status = Command::new("xcodebuild")
|
||||
.arg("build")
|
||||
.arg("-scheme")
|
||||
.arg(name)
|
||||
.arg("-configuration")
|
||||
.arg(configuration)
|
||||
.arg("-sdk")
|
||||
.arg(sdk)
|
||||
.arg("-arch")
|
||||
.arg(arch)
|
||||
.arg("-derivedDataPath")
|
||||
.arg(&derived_data_path)
|
||||
.arg("BUILD_LIBRARY_FOR_DISTRIBUTION=YES")
|
||||
.arg("OTHER_SWIFT_FLAGS=-no-verify-emitted-module-interface")
|
||||
.current_dir(source)
|
||||
.env_clear()
|
||||
.env("PATH", std::env::var_os("PATH").unwrap_or_default())
|
||||
.status()
|
||||
.unwrap();
|
||||
|
||||
assert!(status.success());
|
||||
|
||||
let lib_out_dir = derived_data_path
|
||||
.join("Build")
|
||||
.join("Products")
|
||||
.join(format!("{configuration}-{sdk}"));
|
||||
|
||||
println!(
|
||||
"cargo::rustc-link-search=framework={}",
|
||||
lib_out_dir.display()
|
||||
);
|
||||
println!("cargo:rerun-if-changed={}", source.display());
|
||||
println!("cargo:rustc-link-search=native={}", lib_out_dir.display());
|
||||
println!("cargo:rustc-link-lib=static={name}");
|
||||
}
|
||||
|
||||
/// Updates the Android manifest by inserting XML content into a specified parent tag.
|
||||
///
|
||||
/// The content is wrapped in auto-generated comments and will replace any existing
|
||||
/// content with the same block identifier.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `block_identifier` - A unique identifier for the block (used in comments)
|
||||
/// * `parent` - The parent XML tag name (e.g., "activity", "application")
|
||||
/// * `insert` - The XML content to insert
|
||||
pub fn update_android_manifest(
|
||||
block_identifier: &str,
|
||||
parent: &str,
|
||||
insert: String,
|
||||
) -> anyhow::Result<()> {
|
||||
use std::{
|
||||
env::var_os,
|
||||
fs::{read_to_string, write},
|
||||
path::PathBuf,
|
||||
};
|
||||
|
||||
if let Some(project_path) = var_os("TAURI_ANDROID_PROJECT_PATH").map(PathBuf::from) {
|
||||
let manifest_path = project_path.join("app/src/main/AndroidManifest.xml");
|
||||
if !manifest_path.exists() {
|
||||
return Ok(());
|
||||
}
|
||||
let manifest = read_to_string(&manifest_path)?;
|
||||
let rewritten = insert_into_xml(&manifest, block_identifier, parent, &insert);
|
||||
if rewritten != manifest {
|
||||
write(&manifest_path, rewritten)?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn xml_block_comment(id: &str) -> String {
|
||||
format!("<!-- {id}. AUTO-GENERATED. DO NOT REMOVE. -->")
|
||||
}
|
||||
|
||||
fn insert_into_xml(xml: &str, block_identifier: &str, parent_tag: &str, contents: &str) -> String {
|
||||
let block_comment = xml_block_comment(block_identifier);
|
||||
|
||||
let mut rewritten = Vec::new();
|
||||
let mut found_block = false;
|
||||
let parent_closing_tag = format!("</{parent_tag}>");
|
||||
for line in xml.split('\n') {
|
||||
if line.contains(&block_comment) {
|
||||
found_block = !found_block;
|
||||
continue;
|
||||
}
|
||||
|
||||
// found previous block which should be removed
|
||||
if found_block {
|
||||
continue;
|
||||
}
|
||||
|
||||
if let Some(index) = line.find(&parent_closing_tag) {
|
||||
let indentation = " ".repeat(index + 4);
|
||||
rewritten.push(format!("{indentation}{block_comment}"));
|
||||
for l in contents.split('\n') {
|
||||
rewritten.push(format!("{indentation}{l}"));
|
||||
}
|
||||
rewritten.push(format!("{indentation}{block_comment}"));
|
||||
}
|
||||
|
||||
rewritten.push(line.to_string());
|
||||
}
|
||||
|
||||
rewritten.join("\n")
|
||||
}
|
||||
4684
apps/tauri/src-tauri/vendor/tauri-utils/src/config.rs
vendored
Normal file
4684
apps/tauri/src-tauri/vendor/tauri-utils/src/config.rs
vendored
Normal file
File diff suppressed because it is too large
Load diff
398
apps/tauri/src-tauri/vendor/tauri-utils/src/config/parse.rs
vendored
Normal file
398
apps/tauri/src-tauri/vendor/tauri-utils/src/config/parse.rs
vendored
Normal file
|
|
@ -0,0 +1,398 @@
|
|||
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
use crate::config::Config;
|
||||
use crate::platform::Target;
|
||||
use json_patch::merge;
|
||||
use serde::de::DeserializeOwned;
|
||||
use serde_json::Value;
|
||||
use std::ffi::OsStr;
|
||||
use std::path::{Path, PathBuf};
|
||||
use thiserror::Error;
|
||||
|
||||
/// All extensions that are possibly supported, but perhaps not enabled.
|
||||
pub const EXTENSIONS_SUPPORTED: &[&str] = &["json", "json5", "toml"];
|
||||
|
||||
/// All configuration formats that are possibly supported, but perhaps not enabled.
|
||||
pub const SUPPORTED_FORMATS: &[ConfigFormat] =
|
||||
&[ConfigFormat::Json, ConfigFormat::Json5, ConfigFormat::Toml];
|
||||
|
||||
/// All configuration formats that are currently enabled.
|
||||
pub const ENABLED_FORMATS: &[ConfigFormat] = &[
|
||||
ConfigFormat::Json,
|
||||
#[cfg(feature = "config-json5")]
|
||||
ConfigFormat::Json5,
|
||||
#[cfg(feature = "config-toml")]
|
||||
ConfigFormat::Toml,
|
||||
];
|
||||
|
||||
/// The available configuration formats.
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
pub enum ConfigFormat {
|
||||
/// The default JSON (tauri.conf.json) format.
|
||||
Json,
|
||||
/// The JSON5 (tauri.conf.json5) format.
|
||||
Json5,
|
||||
/// The TOML (Tauri.toml file) format.
|
||||
Toml,
|
||||
}
|
||||
|
||||
impl ConfigFormat {
|
||||
/// Maps the config format to its file name.
|
||||
pub fn into_file_name(self) -> &'static str {
|
||||
match self {
|
||||
Self::Json => "tauri.conf.json",
|
||||
Self::Json5 => "tauri.conf.json5",
|
||||
Self::Toml => "Tauri.toml",
|
||||
}
|
||||
}
|
||||
|
||||
fn into_platform_file_name(self, target: Target) -> &'static str {
|
||||
match self {
|
||||
Self::Json => match target {
|
||||
Target::MacOS => "tauri.macos.conf.json",
|
||||
Target::Windows => "tauri.windows.conf.json",
|
||||
Target::Linux => "tauri.linux.conf.json",
|
||||
Target::Android => "tauri.android.conf.json",
|
||||
Target::Ios => "tauri.ios.conf.json",
|
||||
},
|
||||
Self::Json5 => match target {
|
||||
Target::MacOS => "tauri.macos.conf.json5",
|
||||
Target::Windows => "tauri.windows.conf.json5",
|
||||
Target::Linux => "tauri.linux.conf.json5",
|
||||
Target::Android => "tauri.android.conf.json5",
|
||||
Target::Ios => "tauri.ios.conf.json5",
|
||||
},
|
||||
Self::Toml => match target {
|
||||
Target::MacOS => "Tauri.macos.toml",
|
||||
Target::Windows => "Tauri.windows.toml",
|
||||
Target::Linux => "Tauri.linux.toml",
|
||||
Target::Android => "Tauri.android.toml",
|
||||
Target::Ios => "Tauri.ios.toml",
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents all the errors that can happen while reading the config.
|
||||
#[derive(Debug, Error)]
|
||||
#[non_exhaustive]
|
||||
pub enum ConfigError {
|
||||
/// Failed to parse from JSON.
|
||||
#[error("unable to parse JSON Tauri config file at {path} because {error}")]
|
||||
FormatJson {
|
||||
/// The path that failed to parse into JSON.
|
||||
path: PathBuf,
|
||||
|
||||
/// The parsing [`serde_json::Error`].
|
||||
error: serde_json::Error,
|
||||
},
|
||||
|
||||
/// Failed to parse from JSON5.
|
||||
#[cfg(feature = "config-json5")]
|
||||
#[error("unable to parse JSON5 Tauri config file at {path} because {error}")]
|
||||
FormatJson5 {
|
||||
/// The path that failed to parse into JSON5.
|
||||
path: PathBuf,
|
||||
|
||||
/// The parsing [`json5::Error`].
|
||||
error: ::json5::Error,
|
||||
},
|
||||
|
||||
/// Failed to parse from TOML.
|
||||
#[cfg(feature = "config-toml")]
|
||||
#[error("unable to parse toml Tauri config file at {path} because {error}")]
|
||||
FormatToml {
|
||||
/// The path that failed to parse into TOML.
|
||||
path: PathBuf,
|
||||
|
||||
/// The parsing [`toml::Error`].
|
||||
error: Box<::toml::de::Error>,
|
||||
},
|
||||
|
||||
/// Unknown config file name encountered.
|
||||
#[error("unsupported format encountered {0}")]
|
||||
UnsupportedFormat(String),
|
||||
|
||||
/// Known file extension encountered, but corresponding parser is not enabled (cargo features).
|
||||
#[error("supported (but disabled) format encountered {extension} - try enabling `{feature}` ")]
|
||||
DisabledFormat {
|
||||
/// The extension encountered.
|
||||
extension: String,
|
||||
|
||||
/// The cargo feature to enable it.
|
||||
feature: String,
|
||||
},
|
||||
|
||||
/// A generic IO error with context of what caused it.
|
||||
#[error("unable to read Tauri config file at {path} because {error}")]
|
||||
Io {
|
||||
/// The path the IO error occurred on.
|
||||
path: PathBuf,
|
||||
|
||||
/// The [`std::io::Error`].
|
||||
error: std::io::Error,
|
||||
},
|
||||
}
|
||||
|
||||
/// Determines if the given folder has a configuration file.
|
||||
pub fn folder_has_configuration_file(target: Target, folder: &Path) -> bool {
|
||||
folder.join(ConfigFormat::Json.into_file_name()).exists()
|
||||
|| folder.join(ConfigFormat::Json5.into_file_name()).exists()
|
||||
|| folder.join(ConfigFormat::Toml.into_file_name()).exists()
|
||||
// platform file names
|
||||
|| folder.join(ConfigFormat::Json.into_platform_file_name(target)).exists()
|
||||
|| folder.join(ConfigFormat::Json5.into_platform_file_name(target)).exists()
|
||||
|| folder.join(ConfigFormat::Toml.into_platform_file_name(target)).exists()
|
||||
}
|
||||
|
||||
/// Determines if the given file path represents a Tauri configuration file.
|
||||
pub fn is_configuration_file(target: Target, path: &Path) -> bool {
|
||||
path
|
||||
.file_name()
|
||||
.map(|file_name| {
|
||||
file_name == OsStr::new(ConfigFormat::Json.into_file_name())
|
||||
|| file_name == OsStr::new(ConfigFormat::Json5.into_file_name())
|
||||
|| file_name == OsStr::new(ConfigFormat::Toml.into_file_name())
|
||||
// platform file names
|
||||
|| file_name == OsStr::new(ConfigFormat::Json.into_platform_file_name(target))
|
||||
|| file_name == OsStr::new(ConfigFormat::Json5.into_platform_file_name(target))
|
||||
|| file_name == OsStr::new(ConfigFormat::Toml.into_platform_file_name(target))
|
||||
})
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
/// Reads the configuration from the given root directory.
|
||||
///
|
||||
/// It first looks for a `tauri.conf.json[5]` or `Tauri.toml` file on the given directory. The file must exist.
|
||||
/// Then it looks for a platform-specific configuration file:
|
||||
/// - `tauri.macos.conf.json[5]` or `Tauri.macos.toml` on macOS
|
||||
/// - `tauri.linux.conf.json[5]` or `Tauri.linux.toml` on Linux
|
||||
/// - `tauri.windows.conf.json[5]` or `Tauri.windows.toml` on Windows
|
||||
/// - `tauri.android.conf.json[5]` or `Tauri.android.toml` on Android
|
||||
/// - `tauri.ios.conf.json[5]` or `Tauri.ios.toml` on iOS
|
||||
/// Merging the configurations using [JSON Merge Patch (RFC 7396)].
|
||||
///
|
||||
/// Returns the raw configuration and used config paths.
|
||||
///
|
||||
/// [JSON Merge Patch (RFC 7396)]: https://datatracker.ietf.org/doc/html/rfc7396.
|
||||
pub fn read_from(target: Target, root_dir: &Path) -> Result<(Value, Vec<PathBuf>), ConfigError> {
|
||||
let (mut config, config_file_path) = parse_value(target, root_dir.join("tauri.conf.json"))?;
|
||||
let mut config_paths = vec![config_file_path];
|
||||
if let Some((platform_config, path)) = read_platform(target, root_dir)? {
|
||||
config_paths.push(path);
|
||||
merge(&mut config, &platform_config);
|
||||
}
|
||||
Ok((config, config_paths))
|
||||
}
|
||||
|
||||
/// Reads the platform-specific configuration file from the given root directory if it exists.
|
||||
///
|
||||
/// Check [`read_from`] for more information.
|
||||
pub fn read_platform(
|
||||
target: Target,
|
||||
root_dir: &Path,
|
||||
) -> Result<Option<(Value, PathBuf)>, ConfigError> {
|
||||
let platform_config_path = root_dir.join(ConfigFormat::Json.into_platform_file_name(target));
|
||||
if does_supported_file_name_exist(target, &platform_config_path) {
|
||||
let (platform_config, path): (Value, PathBuf) = parse_value(target, platform_config_path)?;
|
||||
Ok(Some((platform_config, path)))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if a supported config file exists at path.
|
||||
///
|
||||
/// The passed path is expected to be the path to the "default" configuration format, in this case
|
||||
/// JSON with `.json`.
|
||||
pub fn does_supported_file_name_exist(target: Target, path: impl Into<PathBuf>) -> bool {
|
||||
let path = path.into();
|
||||
let source_file_name = path.file_name().unwrap();
|
||||
let lookup_platform_config = ENABLED_FORMATS
|
||||
.iter()
|
||||
.any(|format| source_file_name == format.into_platform_file_name(target));
|
||||
ENABLED_FORMATS.iter().any(|format| {
|
||||
path
|
||||
.with_file_name(if lookup_platform_config {
|
||||
format.into_platform_file_name(target)
|
||||
} else {
|
||||
format.into_file_name()
|
||||
})
|
||||
.exists()
|
||||
})
|
||||
}
|
||||
|
||||
/// Parse the config from path, including alternative formats.
|
||||
///
|
||||
/// Hierarchy:
|
||||
/// 1. Check if `tauri.conf.json` exists
|
||||
/// a. Parse it with `serde_json`
|
||||
/// b. Parse it with `json5` if `serde_json` fails
|
||||
/// c. Return original `serde_json` error if all above steps failed
|
||||
/// 2. Check if `tauri.conf.json5` exists
|
||||
/// a. Parse it with `json5`
|
||||
/// b. Return error if all above steps failed
|
||||
/// 3. Check if `Tauri.json` exists
|
||||
/// a. Parse it with `toml`
|
||||
/// b. Return error if all above steps failed
|
||||
/// 4. Return error if all above steps failed
|
||||
pub fn parse(target: Target, path: impl Into<PathBuf>) -> Result<(Config, PathBuf), ConfigError> {
|
||||
do_parse(target, path.into())
|
||||
}
|
||||
|
||||
/// See [`parse`] for specifics, returns a JSON [`Value`] instead of [`Config`].
|
||||
pub fn parse_value(
|
||||
target: Target,
|
||||
path: impl Into<PathBuf>,
|
||||
) -> Result<(Value, PathBuf), ConfigError> {
|
||||
do_parse(target, path.into())
|
||||
}
|
||||
|
||||
fn do_parse<D: DeserializeOwned>(
|
||||
target: Target,
|
||||
path: PathBuf,
|
||||
) -> Result<(D, PathBuf), ConfigError> {
|
||||
let file_name = path
|
||||
.file_name()
|
||||
.map(OsStr::to_string_lossy)
|
||||
.unwrap_or_default();
|
||||
let lookup_platform_config = ENABLED_FORMATS
|
||||
.iter()
|
||||
.any(|format| file_name == format.into_platform_file_name(target));
|
||||
|
||||
let json5 = path.with_file_name(if lookup_platform_config {
|
||||
ConfigFormat::Json5.into_platform_file_name(target)
|
||||
} else {
|
||||
ConfigFormat::Json5.into_file_name()
|
||||
});
|
||||
let toml = path.with_file_name(if lookup_platform_config {
|
||||
ConfigFormat::Toml.into_platform_file_name(target)
|
||||
} else {
|
||||
ConfigFormat::Toml.into_file_name()
|
||||
});
|
||||
|
||||
let path_ext = path
|
||||
.extension()
|
||||
.map(OsStr::to_string_lossy)
|
||||
.unwrap_or_default();
|
||||
|
||||
if path.exists() {
|
||||
let raw = read_to_string(&path)?;
|
||||
|
||||
// to allow us to easily use the compile-time #[cfg], we always bind
|
||||
#[allow(clippy::let_and_return)]
|
||||
let json = do_parse_json(&raw, &path);
|
||||
|
||||
// we also want to support **valid** json5 in the .json extension if the feature is enabled.
|
||||
// if the json5 is not valid the serde_json error for regular json will be returned.
|
||||
// this could be a bit confusing, so we may want to encourage users using json5 to use the
|
||||
// .json5 extension instead of .json
|
||||
#[cfg(feature = "config-json5")]
|
||||
let json = {
|
||||
match do_parse_json5(&raw, &path) {
|
||||
json5 @ Ok(_) => json5,
|
||||
|
||||
// assume any errors from json5 in a .json file is because it's not json5
|
||||
Err(_) => json,
|
||||
}
|
||||
};
|
||||
|
||||
json.map(|j| (j, path))
|
||||
} else if json5.exists() {
|
||||
#[cfg(feature = "config-json5")]
|
||||
{
|
||||
let raw = read_to_string(&json5)?;
|
||||
do_parse_json5(&raw, &json5).map(|config| (config, json5))
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "config-json5"))]
|
||||
Err(ConfigError::DisabledFormat {
|
||||
extension: ".json5".into(),
|
||||
feature: "config-json5".into(),
|
||||
})
|
||||
} else if toml.exists() {
|
||||
#[cfg(feature = "config-toml")]
|
||||
{
|
||||
let raw = read_to_string(&toml)?;
|
||||
do_parse_toml(&raw, &toml).map(|config| (config, toml))
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "config-toml"))]
|
||||
Err(ConfigError::DisabledFormat {
|
||||
extension: ".toml".into(),
|
||||
feature: "config-toml".into(),
|
||||
})
|
||||
} else if !EXTENSIONS_SUPPORTED.contains(&path_ext.as_ref()) {
|
||||
Err(ConfigError::UnsupportedFormat(path_ext.to_string()))
|
||||
} else {
|
||||
Err(ConfigError::Io {
|
||||
path,
|
||||
error: std::io::ErrorKind::NotFound.into(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// "Low-level" helper to parse JSON into a [`Config`].
|
||||
///
|
||||
/// `raw` should be the contents of the file that is represented by `path`.
|
||||
pub fn parse_json(raw: &str, path: &Path) -> Result<Config, ConfigError> {
|
||||
do_parse_json(raw, path)
|
||||
}
|
||||
|
||||
/// "Low-level" helper to parse JSON into a JSON [`Value`].
|
||||
///
|
||||
/// `raw` should be the contents of the file that is represented by `path`.
|
||||
pub fn parse_json_value(raw: &str, path: &Path) -> Result<Value, ConfigError> {
|
||||
do_parse_json(raw, path)
|
||||
}
|
||||
|
||||
fn do_parse_json<D: DeserializeOwned>(raw: &str, path: &Path) -> Result<D, ConfigError> {
|
||||
serde_json::from_str(raw).map_err(|error| ConfigError::FormatJson {
|
||||
path: path.into(),
|
||||
error,
|
||||
})
|
||||
}
|
||||
|
||||
/// "Low-level" helper to parse JSON5 into a [`Config`].
|
||||
///
|
||||
/// `raw` should be the contents of the file that is represented by `path`. This function requires
|
||||
/// the `config-json5` feature to be enabled.
|
||||
#[cfg(feature = "config-json5")]
|
||||
pub fn parse_json5(raw: &str, path: &Path) -> Result<Config, ConfigError> {
|
||||
do_parse_json5(raw, path)
|
||||
}
|
||||
|
||||
/// "Low-level" helper to parse JSON5 into a JSON [`Value`].
|
||||
///
|
||||
/// `raw` should be the contents of the file that is represented by `path`. This function requires
|
||||
/// the `config-json5` feature to be enabled.
|
||||
#[cfg(feature = "config-json5")]
|
||||
pub fn parse_json5_value(raw: &str, path: &Path) -> Result<Value, ConfigError> {
|
||||
do_parse_json5(raw, path)
|
||||
}
|
||||
|
||||
#[cfg(feature = "config-json5")]
|
||||
fn do_parse_json5<D: DeserializeOwned>(raw: &str, path: &Path) -> Result<D, ConfigError> {
|
||||
::json5::from_str(raw).map_err(|error| ConfigError::FormatJson5 {
|
||||
path: path.into(),
|
||||
error,
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(feature = "config-toml")]
|
||||
fn do_parse_toml<D: DeserializeOwned>(raw: &str, path: &Path) -> Result<D, ConfigError> {
|
||||
::toml::from_str(raw).map_err(|error| ConfigError::FormatToml {
|
||||
path: path.into(),
|
||||
error: Box::new(error),
|
||||
})
|
||||
}
|
||||
|
||||
/// Helper function to wrap IO errors from [`std::fs::read_to_string`] into a [`ConfigError`].
|
||||
fn read_to_string(path: &Path) -> Result<String, ConfigError> {
|
||||
std::fs::read_to_string(path).map_err(|error| ConfigError::Io {
|
||||
path: path.into(),
|
||||
error,
|
||||
})
|
||||
}
|
||||
3088
apps/tauri/src-tauri/vendor/tauri-utils/src/config_v1/mod.rs
vendored
Normal file
3088
apps/tauri/src-tauri/vendor/tauri-utils/src/config_v1/mod.rs
vendored
Normal file
File diff suppressed because it is too large
Load diff
255
apps/tauri/src-tauri/vendor/tauri-utils/src/config_v1/parse.rs
vendored
Normal file
255
apps/tauri/src-tauri/vendor/tauri-utils/src/config_v1/parse.rs
vendored
Normal file
|
|
@ -0,0 +1,255 @@
|
|||
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
#![allow(clippy::result_large_err)]
|
||||
|
||||
use serde::de::DeserializeOwned;
|
||||
use serde_json::Value;
|
||||
use std::ffi::OsStr;
|
||||
use std::path::{Path, PathBuf};
|
||||
use thiserror::Error;
|
||||
|
||||
/// All extensions that are possibly supported, but perhaps not enabled.
|
||||
const EXTENSIONS_SUPPORTED: &[&str] = &["json", "json5", "toml"];
|
||||
|
||||
/// All configuration formats that are currently enabled.
|
||||
const ENABLED_FORMATS: &[ConfigFormat] = &[
|
||||
ConfigFormat::Json,
|
||||
#[cfg(feature = "config-json5")]
|
||||
ConfigFormat::Json5,
|
||||
#[cfg(feature = "config-toml")]
|
||||
ConfigFormat::Toml,
|
||||
];
|
||||
|
||||
/// The available configuration formats.
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
enum ConfigFormat {
|
||||
/// The default JSON (tauri.conf.json) format.
|
||||
Json,
|
||||
/// The JSON5 (tauri.conf.json5) format.
|
||||
Json5,
|
||||
/// The TOML (Tauri.toml file) format.
|
||||
Toml,
|
||||
}
|
||||
|
||||
impl ConfigFormat {
|
||||
/// Maps the config format to its file name.
|
||||
fn into_file_name(self) -> &'static str {
|
||||
match self {
|
||||
Self::Json => "tauri.conf.json",
|
||||
Self::Json5 => "tauri.conf.json5",
|
||||
Self::Toml => "Tauri.toml",
|
||||
}
|
||||
}
|
||||
|
||||
fn into_platform_file_name(self) -> &'static str {
|
||||
match self {
|
||||
Self::Json => {
|
||||
if cfg!(target_os = "macos") {
|
||||
"tauri.macos.conf.json"
|
||||
} else if cfg!(windows) {
|
||||
"tauri.windows.conf.json"
|
||||
} else {
|
||||
"tauri.linux.conf.json"
|
||||
}
|
||||
}
|
||||
Self::Json5 => {
|
||||
if cfg!(target_os = "macos") {
|
||||
"tauri.macos.conf.json5"
|
||||
} else if cfg!(windows) {
|
||||
"tauri.windows.conf.json5"
|
||||
} else {
|
||||
"tauri.linux.conf.json5"
|
||||
}
|
||||
}
|
||||
Self::Toml => {
|
||||
if cfg!(target_os = "macos") {
|
||||
"Tauri.macos.toml"
|
||||
} else if cfg!(windows) {
|
||||
"Tauri.windows.toml"
|
||||
} else {
|
||||
"Tauri.linux.toml"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents all the errors that can happen while reading the config.
|
||||
#[derive(Debug, Error)]
|
||||
#[non_exhaustive]
|
||||
pub enum ConfigError {
|
||||
/// Failed to parse from JSON.
|
||||
#[error("unable to parse JSON Tauri config file at {path} because {error}")]
|
||||
FormatJson {
|
||||
/// The path that failed to parse into JSON.
|
||||
path: PathBuf,
|
||||
|
||||
/// The parsing [`serde_json::Error`].
|
||||
error: serde_json::Error,
|
||||
},
|
||||
|
||||
/// Failed to parse from JSON5.
|
||||
#[cfg(feature = "config-json5")]
|
||||
#[error("unable to parse JSON5 Tauri config file at {path} because {error}")]
|
||||
FormatJson5 {
|
||||
/// The path that failed to parse into JSON5.
|
||||
path: PathBuf,
|
||||
|
||||
/// The parsing [`json5::Error`].
|
||||
error: ::json5::Error,
|
||||
},
|
||||
|
||||
/// Failed to parse from TOML.
|
||||
#[cfg(feature = "config-toml")]
|
||||
#[error("unable to parse toml Tauri config file at {path} because {error}")]
|
||||
FormatToml {
|
||||
/// The path that failed to parse into TOML.
|
||||
path: PathBuf,
|
||||
|
||||
/// The parsing [`toml::Error`].
|
||||
error: ::toml::de::Error,
|
||||
},
|
||||
|
||||
/// Unknown config file name encountered.
|
||||
#[error("unsupported format encountered {0}")]
|
||||
UnsupportedFormat(String),
|
||||
|
||||
/// Known file extension encountered, but corresponding parser is not enabled (cargo features).
|
||||
#[error("supported (but disabled) format encountered {extension} - try enabling `{feature}` ")]
|
||||
DisabledFormat {
|
||||
/// The extension encountered.
|
||||
extension: String,
|
||||
|
||||
/// The cargo feature to enable it.
|
||||
feature: String,
|
||||
},
|
||||
|
||||
/// A generic IO error with context of what caused it.
|
||||
#[error("unable to read Tauri config file at {path} because {error}")]
|
||||
Io {
|
||||
/// The path the IO error occurred on.
|
||||
path: PathBuf,
|
||||
|
||||
/// The [`std::io::Error`].
|
||||
error: std::io::Error,
|
||||
},
|
||||
}
|
||||
|
||||
/// See [`parse`] for specifics, returns a JSON [`Value`] instead of [`Config`].
|
||||
pub fn parse_value(path: impl Into<PathBuf>) -> Result<(Value, PathBuf), ConfigError> {
|
||||
do_parse(path.into())
|
||||
}
|
||||
|
||||
fn do_parse<D: DeserializeOwned>(path: PathBuf) -> Result<(D, PathBuf), ConfigError> {
|
||||
let file_name = path
|
||||
.file_name()
|
||||
.map(OsStr::to_string_lossy)
|
||||
.unwrap_or_default();
|
||||
let lookup_platform_config = ENABLED_FORMATS
|
||||
.iter()
|
||||
.any(|format| file_name == format.into_platform_file_name());
|
||||
|
||||
let json5 = path.with_file_name(if lookup_platform_config {
|
||||
ConfigFormat::Json5.into_platform_file_name()
|
||||
} else {
|
||||
ConfigFormat::Json5.into_file_name()
|
||||
});
|
||||
let toml = path.with_file_name(if lookup_platform_config {
|
||||
ConfigFormat::Toml.into_platform_file_name()
|
||||
} else {
|
||||
ConfigFormat::Toml.into_file_name()
|
||||
});
|
||||
|
||||
let path_ext = path
|
||||
.extension()
|
||||
.map(OsStr::to_string_lossy)
|
||||
.unwrap_or_default();
|
||||
|
||||
if path.exists() {
|
||||
let raw = read_to_string(&path)?;
|
||||
|
||||
// to allow us to easily use the compile-time #[cfg], we always bind
|
||||
#[allow(clippy::let_and_return)]
|
||||
let json = do_parse_json(&raw, &path);
|
||||
|
||||
// we also want to support **valid** json5 in the .json extension if the feature is enabled.
|
||||
// if the json5 is not valid the serde_json error for regular json will be returned.
|
||||
// this could be a bit confusing, so we may want to encourage users using json5 to use the
|
||||
// .json5 extension instead of .json
|
||||
#[cfg(feature = "config-json5")]
|
||||
let json = {
|
||||
match do_parse_json5(&raw, &path) {
|
||||
json5 @ Ok(_) => json5,
|
||||
|
||||
// assume any errors from json5 in a .json file is because it's not json5
|
||||
Err(_) => json,
|
||||
}
|
||||
};
|
||||
|
||||
json.map(|j| (j, path))
|
||||
} else if json5.exists() {
|
||||
#[cfg(feature = "config-json5")]
|
||||
{
|
||||
let raw = read_to_string(&json5)?;
|
||||
do_parse_json5(&raw, &path).map(|config| (config, json5))
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "config-json5"))]
|
||||
Err(ConfigError::DisabledFormat {
|
||||
extension: ".json5".into(),
|
||||
feature: "config-json5".into(),
|
||||
})
|
||||
} else if toml.exists() {
|
||||
#[cfg(feature = "config-toml")]
|
||||
{
|
||||
let raw = read_to_string(&toml)?;
|
||||
do_parse_toml(&raw, &path).map(|config| (config, toml))
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "config-toml"))]
|
||||
Err(ConfigError::DisabledFormat {
|
||||
extension: ".toml".into(),
|
||||
feature: "config-toml".into(),
|
||||
})
|
||||
} else if !EXTENSIONS_SUPPORTED.contains(&path_ext.as_ref()) {
|
||||
Err(ConfigError::UnsupportedFormat(path_ext.to_string()))
|
||||
} else {
|
||||
Err(ConfigError::Io {
|
||||
path,
|
||||
error: std::io::ErrorKind::NotFound.into(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn do_parse_json<D: DeserializeOwned>(raw: &str, path: &Path) -> Result<D, ConfigError> {
|
||||
serde_json::from_str(raw).map_err(|error| ConfigError::FormatJson {
|
||||
path: path.into(),
|
||||
error,
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(feature = "config-json5")]
|
||||
fn do_parse_json5<D: DeserializeOwned>(raw: &str, path: &Path) -> Result<D, ConfigError> {
|
||||
::json5::from_str(raw).map_err(|error| ConfigError::FormatJson5 {
|
||||
path: path.into(),
|
||||
error,
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(feature = "config-toml")]
|
||||
fn do_parse_toml<D: DeserializeOwned>(raw: &str, path: &Path) -> Result<D, ConfigError> {
|
||||
::toml::from_str(raw).map_err(|error| ConfigError::FormatToml {
|
||||
path: path.into(),
|
||||
error,
|
||||
})
|
||||
}
|
||||
|
||||
/// Helper function to wrap IO errors from [`std::fs::read_to_string`] into a [`ConfigError`].
|
||||
fn read_to_string(path: &Path) -> Result<String, ConfigError> {
|
||||
std::fs::read_to_string(path).map_err(|error| ConfigError::Io {
|
||||
path: path.into(),
|
||||
error,
|
||||
})
|
||||
}
|
||||
442
apps/tauri/src-tauri/vendor/tauri-utils/src/html.rs
vendored
Normal file
442
apps/tauri/src-tauri/vendor/tauri-utils/src/html.rs
vendored
Normal file
|
|
@ -0,0 +1,442 @@
|
|||
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
//! The module to process HTML in Tauri.
|
||||
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use html5ever::{
|
||||
LocalName,
|
||||
interface::QualName,
|
||||
namespace_url, ns,
|
||||
serialize::{HtmlSerializer, SerializeOpts, Serializer, TraversalScope},
|
||||
tendril::TendrilSink,
|
||||
};
|
||||
pub use kuchiki::NodeRef;
|
||||
use kuchiki::{Attribute, ExpandedName, NodeData};
|
||||
use serde::Serialize;
|
||||
#[cfg(feature = "isolation")]
|
||||
use serialize_to_javascript::DefaultTemplate;
|
||||
|
||||
#[cfg(feature = "isolation")]
|
||||
use crate::pattern::isolation::IsolationJavascriptCodegen;
|
||||
use crate::{
|
||||
assets::{SCRIPT_NONCE_TOKEN, STYLE_NONCE_TOKEN},
|
||||
config::{DisabledCspModificationKind, PatternKind},
|
||||
};
|
||||
|
||||
// taken from <https://github.com/kuchiki-rs/kuchiki/blob/57ee6920d835315a498e748ba4b07a851ae5e498/src/serializer.rs#L12>
|
||||
fn serialize_node_ref_internal<S: Serializer>(
|
||||
node: &NodeRef,
|
||||
serializer: &mut S,
|
||||
traversal_scope: TraversalScope,
|
||||
) -> crate::Result<()> {
|
||||
match (traversal_scope, node.data()) {
|
||||
(ref scope, NodeData::Element(element)) => {
|
||||
if *scope == TraversalScope::IncludeNode {
|
||||
let attrs = element.attributes.borrow();
|
||||
|
||||
// Unfortunately we need to allocate something to hold these &'a QualName
|
||||
let attrs = attrs
|
||||
.map
|
||||
.iter()
|
||||
.map(|(name, attr)| {
|
||||
(
|
||||
QualName::new(attr.prefix.clone(), name.ns.clone(), name.local.clone()),
|
||||
&attr.value,
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
serializer.start_elem(
|
||||
element.name.clone(),
|
||||
attrs.iter().map(|&(ref name, value)| (name, &**value)),
|
||||
)?
|
||||
}
|
||||
|
||||
let children = match element.template_contents.as_ref() {
|
||||
Some(template_root) => template_root.children(),
|
||||
None => node.children(),
|
||||
};
|
||||
for child in children {
|
||||
serialize_node_ref_internal(&child, serializer, TraversalScope::IncludeNode)?
|
||||
}
|
||||
|
||||
if *scope == TraversalScope::IncludeNode {
|
||||
serializer.end_elem(element.name.clone())?
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
(_, &NodeData::DocumentFragment) | (_, &NodeData::Document(_)) => {
|
||||
for child in node.children() {
|
||||
serialize_node_ref_internal(&child, serializer, TraversalScope::IncludeNode)?
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
(TraversalScope::ChildrenOnly(_), _) => Ok(()),
|
||||
|
||||
(TraversalScope::IncludeNode, NodeData::Doctype(doctype)) => {
|
||||
serializer.write_doctype(&doctype.name).map_err(Into::into)
|
||||
}
|
||||
(TraversalScope::IncludeNode, NodeData::Text(text)) => {
|
||||
serializer.write_text(&text.borrow()).map_err(Into::into)
|
||||
}
|
||||
(TraversalScope::IncludeNode, NodeData::Comment(text)) => {
|
||||
serializer.write_comment(&text.borrow()).map_err(Into::into)
|
||||
}
|
||||
(TraversalScope::IncludeNode, NodeData::ProcessingInstruction(contents)) => {
|
||||
let contents = contents.borrow();
|
||||
serializer
|
||||
.write_processing_instruction(&contents.0, &contents.1)
|
||||
.map_err(Into::into)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Serializes the node to HTML.
|
||||
pub fn serialize_node(node: &NodeRef) -> Vec<u8> {
|
||||
let mut u8_vec = Vec::new();
|
||||
let mut ser = HtmlSerializer::new(
|
||||
&mut u8_vec,
|
||||
SerializeOpts {
|
||||
traversal_scope: TraversalScope::IncludeNode,
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
serialize_node_ref_internal(node, &mut ser, TraversalScope::IncludeNode).unwrap();
|
||||
u8_vec
|
||||
}
|
||||
|
||||
/// Parses the given HTML string.
|
||||
pub fn parse(html: String) -> NodeRef {
|
||||
kuchiki::parse_html().one(html).document_node
|
||||
}
|
||||
|
||||
fn with_head<F: FnOnce(&NodeRef)>(document: &NodeRef, f: F) {
|
||||
if let Ok(ref node) = document.select_first("head") {
|
||||
f(node.as_node())
|
||||
} else {
|
||||
let node = NodeRef::new_element(
|
||||
QualName::new(None, ns!(html), LocalName::from("head")),
|
||||
None,
|
||||
);
|
||||
f(&node);
|
||||
document.prepend(node)
|
||||
}
|
||||
}
|
||||
|
||||
fn inject_nonce(document: &NodeRef, selector: &str, token: &str) {
|
||||
if let Ok(elements) = document.select(selector) {
|
||||
for target in elements {
|
||||
let node = target.as_node();
|
||||
let element = node.as_element().unwrap();
|
||||
|
||||
let mut attrs = element.attributes.borrow_mut();
|
||||
// if the node already has the `nonce` attribute, skip it
|
||||
if attrs.get("nonce").is_some() {
|
||||
continue;
|
||||
}
|
||||
attrs.insert("nonce", token.into());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Inject nonce tokens to all scripts and styles.
|
||||
pub fn inject_nonce_token(
|
||||
document: &NodeRef,
|
||||
dangerous_disable_asset_csp_modification: &DisabledCspModificationKind,
|
||||
) {
|
||||
if dangerous_disable_asset_csp_modification.can_modify("script-src") {
|
||||
inject_nonce(document, "script[src^='http']", SCRIPT_NONCE_TOKEN);
|
||||
}
|
||||
if dangerous_disable_asset_csp_modification.can_modify("style-src") {
|
||||
inject_nonce(document, "style", STYLE_NONCE_TOKEN);
|
||||
}
|
||||
}
|
||||
|
||||
/// Injects a content security policy to the HTML.
|
||||
pub fn inject_csp(document: &NodeRef, csp: &str) {
|
||||
with_head(document, |head| {
|
||||
head.append(create_csp_meta_tag(csp));
|
||||
});
|
||||
}
|
||||
|
||||
fn create_csp_meta_tag(csp: &str) -> NodeRef {
|
||||
NodeRef::new_element(
|
||||
QualName::new(None, ns!(html), LocalName::from("meta")),
|
||||
vec![
|
||||
(
|
||||
ExpandedName::new(ns!(), LocalName::from("http-equiv")),
|
||||
Attribute {
|
||||
prefix: None,
|
||||
value: "Content-Security-Policy".into(),
|
||||
},
|
||||
),
|
||||
(
|
||||
ExpandedName::new(ns!(), LocalName::from("content")),
|
||||
Attribute {
|
||||
prefix: None,
|
||||
value: csp.into(),
|
||||
},
|
||||
),
|
||||
],
|
||||
)
|
||||
}
|
||||
|
||||
/// The shape of the JavaScript Pattern config
|
||||
#[derive(Debug, Serialize)]
|
||||
#[serde(rename_all = "lowercase", tag = "pattern")]
|
||||
pub enum PatternObject {
|
||||
/// Brownfield pattern.
|
||||
Brownfield,
|
||||
/// Isolation pattern. Recommended for security purposes.
|
||||
Isolation {
|
||||
/// Which `IsolationSide` this `PatternObject` is getting injected into
|
||||
side: IsolationSide,
|
||||
},
|
||||
}
|
||||
|
||||
impl From<&PatternKind> for PatternObject {
|
||||
fn from(pattern_kind: &PatternKind) -> Self {
|
||||
match pattern_kind {
|
||||
PatternKind::Brownfield => Self::Brownfield,
|
||||
PatternKind::Isolation { .. } => Self::Isolation {
|
||||
side: IsolationSide::default(),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Where the JavaScript is injected to
|
||||
#[derive(Debug, Serialize, Default)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum IsolationSide {
|
||||
/// Original frame, the Brownfield application
|
||||
#[default]
|
||||
Original,
|
||||
/// Secure frame, the isolation security application
|
||||
Secure,
|
||||
}
|
||||
|
||||
/// Injects the Isolation JavaScript to a codegen time document.
|
||||
///
|
||||
/// Note: This function is not considered part of the stable API.
|
||||
#[cfg(feature = "isolation")]
|
||||
pub fn inject_codegen_isolation_script(document: &NodeRef) {
|
||||
with_head(document, |head| {
|
||||
let script = NodeRef::new_element(
|
||||
QualName::new(None, ns!(html), "script".into()),
|
||||
vec![(
|
||||
ExpandedName::new(ns!(), LocalName::from("nonce")),
|
||||
Attribute {
|
||||
prefix: None,
|
||||
value: SCRIPT_NONCE_TOKEN.into(),
|
||||
},
|
||||
)],
|
||||
);
|
||||
script.append(NodeRef::new_text(
|
||||
IsolationJavascriptCodegen {}
|
||||
.render_default(&Default::default())
|
||||
.expect("unable to render codegen isolation script template")
|
||||
.into_string(),
|
||||
));
|
||||
|
||||
head.prepend(script);
|
||||
});
|
||||
}
|
||||
|
||||
/// Temporary workaround for Windows not allowing requests
|
||||
///
|
||||
/// Note: this does not prevent path traversal due to the isolation application expectation that it
|
||||
/// is secure.
|
||||
pub fn inline_isolation(document: &NodeRef, dir: &Path) {
|
||||
for script in document
|
||||
.select("script[src]")
|
||||
.expect("unable to parse document for scripts")
|
||||
{
|
||||
let src = {
|
||||
let attributes = script.attributes.borrow();
|
||||
attributes
|
||||
.get(LocalName::from("src"))
|
||||
.expect("script with src attribute has no src value")
|
||||
.to_string()
|
||||
};
|
||||
|
||||
let mut path = PathBuf::from(src);
|
||||
if path.has_root() {
|
||||
path = path
|
||||
.strip_prefix("/")
|
||||
.expect("Tauri \"Isolation\" Pattern only supports relative or absolute (`/`) paths.")
|
||||
.into();
|
||||
}
|
||||
|
||||
let file = std::fs::read_to_string(dir.join(path)).expect("unable to find isolation file");
|
||||
script.as_node().append(NodeRef::new_text(file));
|
||||
|
||||
let mut attributes = script.attributes.borrow_mut();
|
||||
attributes.remove(LocalName::from("src"));
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Verify this, this is not found in the HTML spec, see https://github.com/tauri-apps/tauri/pull/14265#discussion_r2415396842
|
||||
/// Normalize line endings in script content to match what the browser uses for CSP hashing.
|
||||
///
|
||||
/// According to the HTML spec, browsers normalize:
|
||||
/// - `\r\n` → `\n`
|
||||
/// - `\r` → `\n`
|
||||
pub fn normalize_script_for_csp(input: &[u8]) -> Vec<u8> {
|
||||
let mut output = Vec::with_capacity(input.len());
|
||||
|
||||
let mut i = 0;
|
||||
while i < input.len() {
|
||||
match input[i] {
|
||||
b'\r' => {
|
||||
if i + 1 < input.len() && input[i + 1] == b'\n' {
|
||||
// CRLF → LF
|
||||
output.push(b'\n');
|
||||
i += 2;
|
||||
} else {
|
||||
// Lone CR → LF
|
||||
output.push(b'\n');
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
output.push(input[i]);
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
output
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::io::Write;
|
||||
|
||||
use super::*;
|
||||
use crate::{
|
||||
assets::{SCRIPT_NONCE_TOKEN, STYLE_NONCE_TOKEN},
|
||||
config,
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn csp() {
|
||||
let htmls = vec![
|
||||
"<html><head></head></html>".to_string(),
|
||||
"<html></html>".to_string(),
|
||||
];
|
||||
|
||||
for html in htmls {
|
||||
let document = parse(html);
|
||||
let csp = "csp-string";
|
||||
inject_csp(&document, csp);
|
||||
|
||||
assert_eq!(
|
||||
String::from_utf8(serialize_node(&document)).unwrap(),
|
||||
format!(
|
||||
r#"<html><head><meta http-equiv="Content-Security-Policy" content="{csp}"></head><body></body></html>"#,
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn normalize_script_for_csp_test() {
|
||||
let js = "// Copyright 2019-2024 Tauri Programme within The Commons Conservancy\r// SPDX-License-Identifier: Apache-2.0\n// SPDX-License-Identifier: MIT\r\n\r\nwindow.__TAURI_ISOLATION_HOOK__ = (payload, options) => {\r\n return payload\r\n}\r\n";
|
||||
let expected = "// Copyright 2019-2024 Tauri Programme within The Commons Conservancy\n// SPDX-License-Identifier: Apache-2.0\n// SPDX-License-Identifier: MIT\n\nwindow.__TAURI_ISOLATION_HOOK__ = (payload, options) => {\n return payload\n}\n";
|
||||
|
||||
assert_eq!(normalize_script_for_csp(js.as_bytes()), expected.as_bytes())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_and_serialize_roundtrips() {
|
||||
let htmls = [
|
||||
"<html><head><title>Test</title></head><body><h1>Hello</h1></body></html>",
|
||||
"<!DOCTYPE html><html><head></head><body></body></html>",
|
||||
];
|
||||
|
||||
for html in htmls {
|
||||
let parsed = parse(html.to_string());
|
||||
let serialized = serialize_node(&parsed);
|
||||
let result = String::from_utf8(serialized).unwrap();
|
||||
|
||||
assert_eq!(result, html);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn inject_nonce_to_scripts() {
|
||||
let html = r#"<html><head><script src="http://example.com/script.js"></script></head><body></body></html>"#;
|
||||
|
||||
let document = parse(html.to_string());
|
||||
inject_nonce_token(&document, &config::DisabledCspModificationKind::Flag(false));
|
||||
|
||||
assert_eq!(
|
||||
String::from_utf8(serialize_node(&document)).unwrap(),
|
||||
format!(
|
||||
r#"<html><head><script src="http://example.com/script.js" nonce="{SCRIPT_NONCE_TOKEN}"></script></head><body></body></html>"#
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn inject_nonce_to_styles() {
|
||||
let html = r#"<html><head><style>body { color: red; }</style></head><body></body></html>"#;
|
||||
|
||||
let document = parse(html.to_string());
|
||||
inject_nonce_token(&document, &config::DisabledCspModificationKind::Flag(false));
|
||||
|
||||
assert_eq!(
|
||||
String::from_utf8(serialize_node(&document)).unwrap(),
|
||||
format!(
|
||||
r#"<html><head><style nonce="{STYLE_NONCE_TOKEN}">body {{ color: red; }}</style></head><body></body></html>"#
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn inject_nonce_skips_existing() {
|
||||
let html = r#"<html><head><script src="http://example.com/script.js" nonce="existing"></script></head><body></body></html>"#;
|
||||
|
||||
let document = parse(html.to_string());
|
||||
inject_nonce_token(&document, &config::DisabledCspModificationKind::Flag(false));
|
||||
|
||||
assert_eq!(String::from_utf8(serialize_node(&document)).unwrap(), html);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn inject_nonce_respects_disabled_modification() {
|
||||
let html = r#"<html><head><script src="http://example.com/script.js"></script></head><body></body></html>"#;
|
||||
|
||||
let document = parse(html.to_string());
|
||||
inject_nonce_token(&document, &config::DisabledCspModificationKind::Flag(true));
|
||||
|
||||
assert_eq!(
|
||||
String::from_utf8(serialize_node(&document)).unwrap(),
|
||||
r#"<html><head><script src="http://example.com/script.js"></script></head><body></body></html>"#
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn inline_isolation_replaces_src_with_content() {
|
||||
let temp_dir = tempfile::tempdir().unwrap();
|
||||
let mut file = tempfile::NamedTempFile::with_suffix_in(".js", &temp_dir).unwrap();
|
||||
file.write_all(b"console.log('test');").unwrap();
|
||||
let file_name = file.path().file_name().unwrap().to_str().unwrap();
|
||||
|
||||
let html =
|
||||
format!(r#"<html><head><script src="/{file_name}"></script></head><body></body></html>"#);
|
||||
let document = parse(html);
|
||||
inline_isolation(&document, temp_dir.path());
|
||||
|
||||
assert_eq!(
|
||||
String::from_utf8(serialize_node(&document)).unwrap(),
|
||||
r#"<html><head><script>console.log('test');</script></head><body></body></html>"#
|
||||
);
|
||||
}
|
||||
}
|
||||
335
apps/tauri/src-tauri/vendor/tauri-utils/src/html2.rs
vendored
Normal file
335
apps/tauri/src-tauri/vendor/tauri-utils/src/html2.rs
vendored
Normal file
|
|
@ -0,0 +1,335 @@
|
|||
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
//! The module to process HTML in Tauri.
|
||||
//!
|
||||
//! # Stability
|
||||
//!
|
||||
//! This is utility used in Tauri internally and not considered part of the stable API.
|
||||
//! If you use it, note that it may include breaking changes in the future.
|
||||
|
||||
use dom_query::NodeRef;
|
||||
|
||||
use crate::{
|
||||
assets::{SCRIPT_NONCE_TOKEN, STYLE_NONCE_TOKEN},
|
||||
config::DisabledCspModificationKind,
|
||||
};
|
||||
|
||||
/// # Stability
|
||||
///
|
||||
/// This dependency might receive updates in minor releases.
|
||||
pub use dom_query::Document;
|
||||
|
||||
/// Serializes the document to HTML.
|
||||
///
|
||||
/// # Stability
|
||||
///
|
||||
/// This dependency [`dom_query`] for [`Document`] might receive updates in minor releases.
|
||||
pub fn serialize_doc(document: &Document) -> Vec<u8> {
|
||||
document.html().as_bytes().to_vec()
|
||||
}
|
||||
|
||||
/// Parses the given HTML string.
|
||||
///
|
||||
/// # Stability
|
||||
///
|
||||
/// This dependency [`dom_query`] for [`Document`] might receive updates in minor releases.
|
||||
pub fn parse_doc(html: String) -> Document {
|
||||
Document::from(html)
|
||||
}
|
||||
|
||||
fn ensure_head(document: &Document) -> NodeRef<'_> {
|
||||
document.head().unwrap_or_else(|| {
|
||||
let html = document.html_root();
|
||||
let head = document.tree.new_element("head");
|
||||
html.prepend_child(&head);
|
||||
head
|
||||
})
|
||||
}
|
||||
|
||||
fn inject_nonce(document: &Document, selector: &str, token: &str) {
|
||||
let elements = document.select(selector);
|
||||
for elem in elements.nodes() {
|
||||
// if the node already has the `nonce` attribute, skip it
|
||||
if elem.attr("nonce").is_some() {
|
||||
continue;
|
||||
}
|
||||
elem.set_attr("nonce", token);
|
||||
}
|
||||
}
|
||||
|
||||
/// Inject nonce tokens to all scripts and styles.
|
||||
///
|
||||
/// # Stability
|
||||
///
|
||||
/// This dependency [`dom_query`] for [`Document`] might receive updates in minor releases.
|
||||
pub fn inject_nonce_token(
|
||||
document: &Document,
|
||||
dangerous_disable_asset_csp_modification: &DisabledCspModificationKind,
|
||||
) {
|
||||
if dangerous_disable_asset_csp_modification.can_modify("script-src") {
|
||||
inject_nonce(document, "script[src^='http']", SCRIPT_NONCE_TOKEN);
|
||||
}
|
||||
if dangerous_disable_asset_csp_modification.can_modify("style-src") {
|
||||
inject_nonce(document, "style", STYLE_NONCE_TOKEN);
|
||||
}
|
||||
}
|
||||
|
||||
/// Injects a content security policy to the HTML.
|
||||
///
|
||||
/// # Stability
|
||||
///
|
||||
/// This dependency [`dom_query`] for [`Document`] might receive updates in minor releases.
|
||||
pub fn inject_csp(document: &Document, csp: &str) {
|
||||
let head = ensure_head(document);
|
||||
let meta_tag = document.tree.new_element("meta");
|
||||
meta_tag.set_attr("http-equiv", "Content-Security-Policy");
|
||||
meta_tag.set_attr("content", csp);
|
||||
head.append_child(&meta_tag);
|
||||
}
|
||||
|
||||
/// Injects a content security policy to the HTML.
|
||||
///
|
||||
/// # Stability
|
||||
///
|
||||
/// This dependency [`dom_query`] for [`Document`] might receive updates in minor releases.
|
||||
pub fn append_script_to_head(document: &Document, script: &str) {
|
||||
let head = ensure_head(document);
|
||||
let script_tag = document.tree.new_element("script");
|
||||
script_tag.set_text(script);
|
||||
head.prepend_child(&script_tag);
|
||||
}
|
||||
|
||||
/// Injects the Isolation JavaScript to a codegen time document.
|
||||
///
|
||||
/// Note: This function is not considered part of the stable API.
|
||||
///
|
||||
/// # Stability
|
||||
///
|
||||
/// This dependency [`dom_query`] for [`Document`] might receive updates in minor releases.
|
||||
#[cfg(feature = "isolation")]
|
||||
pub fn inject_codegen_isolation_script(document: &Document) {
|
||||
use crate::pattern::isolation::IsolationJavascriptCodegen;
|
||||
use serialize_to_javascript::DefaultTemplate;
|
||||
|
||||
let head = ensure_head(document);
|
||||
|
||||
let script_content = IsolationJavascriptCodegen {}
|
||||
.render_default(&Default::default())
|
||||
.expect("unable to render codegen isolation script template")
|
||||
.into_string();
|
||||
|
||||
let script_tag = document.tree.new_element("script");
|
||||
script_tag.set_attr("nonce", SCRIPT_NONCE_TOKEN);
|
||||
script_tag.set_text(script_content);
|
||||
|
||||
head.prepend_child(&script_tag);
|
||||
}
|
||||
|
||||
/// Temporary workaround for Windows not allowing requests
|
||||
///
|
||||
/// Note: this does not prevent path traversal due to the isolation application expectation that it
|
||||
/// is secure.
|
||||
///
|
||||
/// # Stability
|
||||
///
|
||||
/// This dependency [`dom_query`] for [`Document`] might receive updates in minor releases.
|
||||
#[cfg(feature = "isolation")]
|
||||
pub fn inline_isolation(document: &Document, dir: &std::path::Path) {
|
||||
let scripts = document.select("script[src]");
|
||||
|
||||
for script in scripts.nodes() {
|
||||
let src = match script.attr("src") {
|
||||
Some(s) => s.to_string(),
|
||||
None => continue,
|
||||
};
|
||||
|
||||
let mut path = std::path::PathBuf::from(src);
|
||||
if path.has_root() {
|
||||
path = path
|
||||
.strip_prefix("/")
|
||||
.expect("Tauri \"Isolation\" Pattern only supports relative or absolute (`/`) paths.")
|
||||
.into();
|
||||
}
|
||||
|
||||
let file = std::fs::read_to_string(dir.join(path)).expect("unable to find isolation file");
|
||||
|
||||
script.set_text(file);
|
||||
script.remove_attr("src");
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Verify this, this is not found in the HTML spec, see https://github.com/tauri-apps/tauri/pull/14265#discussion_r2415396842
|
||||
/// Normalize line endings in script content to match what the browser uses for CSP hashing.
|
||||
///
|
||||
/// According to the HTML spec, browsers normalize:
|
||||
/// - `\r\n` → `\n`
|
||||
/// - `\r` → `\n`
|
||||
pub fn normalize_script_for_csp(input: &[u8]) -> Vec<u8> {
|
||||
let mut output = Vec::with_capacity(input.len());
|
||||
|
||||
let mut i = 0;
|
||||
while i < input.len() {
|
||||
match input[i] {
|
||||
b'\r' => {
|
||||
if i + 1 < input.len() && input[i + 1] == b'\n' {
|
||||
// CRLF → LF
|
||||
output.push(b'\n');
|
||||
i += 2;
|
||||
} else {
|
||||
// Lone CR → LF
|
||||
output.push(b'\n');
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
output.push(input[i]);
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
output
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::{
|
||||
assets::{SCRIPT_NONCE_TOKEN, STYLE_NONCE_TOKEN},
|
||||
config,
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn csp() {
|
||||
let htmls = vec![
|
||||
"<html><head></head></html>".to_string(),
|
||||
"<html></html>".to_string(),
|
||||
];
|
||||
|
||||
for html in htmls {
|
||||
let document = parse_doc(html);
|
||||
let csp = "csp-string";
|
||||
inject_csp(&document, csp);
|
||||
|
||||
assert_eq!(
|
||||
String::from_utf8(serialize_doc(&document)).unwrap(),
|
||||
format!(
|
||||
r#"<html><head><meta http-equiv="Content-Security-Policy" content="{csp}"></head><body></body></html>"#
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn normalize_script_for_csp_test() {
|
||||
let js = "// Copyright 2019-2024 Tauri Programme within The Commons Conservancy\r// SPDX-License-Identifier: Apache-2.0\n// SPDX-License-Identifier: MIT\r\n\r\nwindow.__TAURI_ISOLATION_HOOK__ = (payload, options) => {\r\n return payload\r\n}\r\n";
|
||||
let expected = "// Copyright 2019-2024 Tauri Programme within The Commons Conservancy\n// SPDX-License-Identifier: Apache-2.0\n// SPDX-License-Identifier: MIT\n\nwindow.__TAURI_ISOLATION_HOOK__ = (payload, options) => {\n return payload\n}\n";
|
||||
|
||||
assert_eq!(normalize_script_for_csp(js.as_bytes()), expected.as_bytes())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_and_serialize_roundtrips() {
|
||||
let htmls = [
|
||||
"<html><head><title>Test</title></head><body><h1>Hello</h1></body></html>",
|
||||
"<!DOCTYPE html><html><head></head><body></body></html>",
|
||||
];
|
||||
|
||||
for html in htmls {
|
||||
let parsed = parse_doc(html.to_string());
|
||||
let serialized = serialize_doc(&parsed);
|
||||
let result = String::from_utf8(serialized).unwrap();
|
||||
|
||||
assert_eq!(result, html);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn inject_nonce_to_scripts() {
|
||||
let html = r#"<html><head><script src="http://example.com/script.js"></script></head><body></body></html>"#;
|
||||
|
||||
let document = parse_doc(html.to_string());
|
||||
inject_nonce_token(&document, &config::DisabledCspModificationKind::Flag(false));
|
||||
|
||||
assert_eq!(
|
||||
String::from_utf8(serialize_doc(&document)).unwrap(),
|
||||
format!(
|
||||
r#"<html><head><script src="http://example.com/script.js" nonce="{SCRIPT_NONCE_TOKEN}"></script></head><body></body></html>"#
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn inject_nonce_to_styles() {
|
||||
let html = r#"<html><head><style>body { color: red; }</style></head><body></body></html>"#;
|
||||
|
||||
let document = parse_doc(html.to_string());
|
||||
inject_nonce_token(&document, &config::DisabledCspModificationKind::Flag(false));
|
||||
|
||||
assert_eq!(
|
||||
String::from_utf8(serialize_doc(&document)).unwrap(),
|
||||
format!(
|
||||
r#"<html><head><style nonce="{STYLE_NONCE_TOKEN}">body {{ color: red; }}</style></head><body></body></html>"#
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn append_script_to_head_test() {
|
||||
let html = r#"<html><head></head><body></body></html>"#;
|
||||
|
||||
let document = parse_doc(html.to_string());
|
||||
append_script_to_head(&document, r#"console.log('Test')"#);
|
||||
|
||||
assert_eq!(
|
||||
String::from_utf8(serialize_doc(&document)).unwrap(),
|
||||
format!(r#"<html><head><script>console.log('Test')</script></head><body></body></html>"#)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn inject_nonce_skips_existing() {
|
||||
let html = r#"<html><head><script src="http://example.com/script.js" nonce="existing"></script></head><body></body></html>"#;
|
||||
|
||||
let document = parse_doc(html.to_string());
|
||||
inject_nonce_token(&document, &config::DisabledCspModificationKind::Flag(false));
|
||||
|
||||
assert_eq!(String::from_utf8(serialize_doc(&document)).unwrap(), html);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn inject_nonce_respects_disabled_modification() {
|
||||
let html = r#"<html><head><script src="http://example.com/script.js"></script></head><body></body></html>"#;
|
||||
|
||||
let document = parse_doc(html.to_string());
|
||||
inject_nonce_token(&document, &config::DisabledCspModificationKind::Flag(true));
|
||||
|
||||
assert_eq!(
|
||||
String::from_utf8(serialize_doc(&document)).unwrap(),
|
||||
r#"<html><head><script src="http://example.com/script.js"></script></head><body></body></html>"#
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(feature = "isolation")]
|
||||
fn inline_isolation_replaces_src_with_content() {
|
||||
use std::io::Write;
|
||||
|
||||
let temp_dir = tempfile::tempdir().unwrap();
|
||||
let mut file = tempfile::NamedTempFile::with_suffix_in(".js", &temp_dir).unwrap();
|
||||
file.write_all(b"console.log('test');").unwrap();
|
||||
let file_name = file.path().file_name().unwrap().to_str().unwrap();
|
||||
|
||||
let html =
|
||||
format!(r#"<html><head><script src="/{file_name}"></script></head><body></body></html>"#);
|
||||
let document = parse_doc(html);
|
||||
inline_isolation(&document, temp_dir.path());
|
||||
|
||||
assert_eq!(
|
||||
String::from_utf8(serialize_doc(&document)).unwrap(),
|
||||
r#"<html><head><script>console.log('test');</script></head><body></body></html>"#
|
||||
);
|
||||
}
|
||||
}
|
||||
47
apps/tauri/src-tauri/vendor/tauri-utils/src/io.rs
vendored
Normal file
47
apps/tauri/src-tauri/vendor/tauri-utils/src/io.rs
vendored
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
//! IO helpers.
|
||||
|
||||
use std::io::BufRead;
|
||||
|
||||
/// Read all bytes until a newline (the `0xA` byte) or a carriage return (`\r`) is reached, and append them to the provided buffer.
|
||||
///
|
||||
/// Adapted from <https://doc.rust-lang.org/std/io/trait.BufRead.html#method.read_line>.
|
||||
pub fn read_line<R: BufRead + ?Sized>(r: &mut R, buf: &mut Vec<u8>) -> std::io::Result<usize> {
|
||||
let mut read = 0;
|
||||
loop {
|
||||
let (done, used) = {
|
||||
let available = match r.fill_buf() {
|
||||
Ok(n) => n,
|
||||
Err(ref e) if e.kind() == std::io::ErrorKind::Interrupted => continue,
|
||||
|
||||
Err(e) => return Err(e),
|
||||
};
|
||||
match memchr::memchr(b'\n', available) {
|
||||
Some(i) => {
|
||||
let end = i + 1;
|
||||
buf.extend_from_slice(&available[..end]);
|
||||
(true, end)
|
||||
}
|
||||
None => match memchr::memchr(b'\r', available) {
|
||||
Some(i) => {
|
||||
let end = i + 1;
|
||||
buf.extend_from_slice(&available[..end]);
|
||||
(true, end)
|
||||
}
|
||||
None => {
|
||||
buf.extend_from_slice(available);
|
||||
(false, available.len())
|
||||
}
|
||||
},
|
||||
}
|
||||
};
|
||||
r.consume(used);
|
||||
read += used;
|
||||
if done || used == 0 {
|
||||
return Ok(read);
|
||||
}
|
||||
}
|
||||
}
|
||||
404
apps/tauri/src-tauri/vendor/tauri-utils/src/lib.rs
vendored
Normal file
404
apps/tauri/src-tauri/vendor/tauri-utils/src/lib.rs
vendored
Normal file
|
|
@ -0,0 +1,404 @@
|
|||
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
//! This crate contains common code that is reused in many places and offers useful utilities like parsing configuration files, detecting platform triples, injecting the CSP, and managing assets.
|
||||
|
||||
#![doc(
|
||||
html_logo_url = "https://github.com/tauri-apps/tauri/raw/dev/.github/icon.png",
|
||||
html_favicon_url = "https://github.com/tauri-apps/tauri/raw/dev/.github/icon.png"
|
||||
)]
|
||||
#![warn(missing_docs, rust_2018_idioms)]
|
||||
#![allow(clippy::deprecated_semver)]
|
||||
|
||||
use std::{
|
||||
ffi::OsString,
|
||||
fmt::Display,
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
use semver::Version;
|
||||
use serde::{Deserialize, Deserializer, Serialize, Serializer};
|
||||
|
||||
pub mod acl;
|
||||
pub mod assets;
|
||||
pub mod config;
|
||||
pub mod config_v1;
|
||||
#[cfg(feature = "html-manipulation")]
|
||||
pub mod html;
|
||||
#[cfg(feature = "html-manipulation-2")]
|
||||
pub mod html2;
|
||||
pub mod io;
|
||||
pub mod mime_type;
|
||||
pub mod platform;
|
||||
pub mod plugin;
|
||||
/// Prepare application resources and sidecars.
|
||||
#[cfg(feature = "resources")]
|
||||
pub mod resources;
|
||||
#[cfg(any(feature = "build", feature = "build-2"))]
|
||||
pub mod tokens;
|
||||
|
||||
#[cfg(any(feature = "build", feature = "build-2"))]
|
||||
pub mod build;
|
||||
|
||||
/// Application pattern.
|
||||
pub mod pattern;
|
||||
|
||||
/// `tauri::App` package information.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct PackageInfo {
|
||||
/// App name
|
||||
pub name: String,
|
||||
/// App version
|
||||
pub version: Version,
|
||||
/// The crate authors.
|
||||
pub authors: &'static str,
|
||||
/// The crate description.
|
||||
pub description: &'static str,
|
||||
/// The crate name.
|
||||
pub crate_name: &'static str,
|
||||
}
|
||||
|
||||
#[allow(deprecated)]
|
||||
mod window_effects {
|
||||
use super::*;
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Copy, Deserialize, Serialize)]
|
||||
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
/// Platform-specific window effects
|
||||
pub enum WindowEffect {
|
||||
/// A default material appropriate for the view's effectiveAppearance. **macOS 10.14-**
|
||||
#[deprecated(
|
||||
since = "macOS 10.14",
|
||||
note = "You should instead choose an appropriate semantic material."
|
||||
)]
|
||||
AppearanceBased,
|
||||
/// **macOS 10.14-**
|
||||
#[deprecated(since = "macOS 10.14", note = "Use a semantic material instead.")]
|
||||
Light,
|
||||
/// **macOS 10.14-**
|
||||
#[deprecated(since = "macOS 10.14", note = "Use a semantic material instead.")]
|
||||
Dark,
|
||||
/// **macOS 10.14-**
|
||||
#[deprecated(since = "macOS 10.14", note = "Use a semantic material instead.")]
|
||||
MediumLight,
|
||||
/// **macOS 10.14-**
|
||||
#[deprecated(since = "macOS 10.14", note = "Use a semantic material instead.")]
|
||||
UltraDark,
|
||||
/// **macOS 10.10+**
|
||||
Titlebar,
|
||||
/// **macOS 10.10+**
|
||||
Selection,
|
||||
/// **macOS 10.11+**
|
||||
Menu,
|
||||
/// **macOS 10.11+**
|
||||
Popover,
|
||||
/// **macOS 10.11+**
|
||||
Sidebar,
|
||||
/// **macOS 10.14+**
|
||||
HeaderView,
|
||||
/// **macOS 10.14+**
|
||||
Sheet,
|
||||
/// **macOS 10.14+**
|
||||
WindowBackground,
|
||||
/// **macOS 10.14+**
|
||||
HudWindow,
|
||||
/// **macOS 10.14+**
|
||||
FullScreenUI,
|
||||
/// **macOS 10.14+**
|
||||
Tooltip,
|
||||
/// **macOS 10.14+**
|
||||
ContentBackground,
|
||||
/// **macOS 10.14+**
|
||||
UnderWindowBackground,
|
||||
/// **macOS 10.14+**
|
||||
UnderPageBackground,
|
||||
/// Mica effect that matches the system dark preference **Windows 11 Only**
|
||||
Mica,
|
||||
/// Mica effect with dark mode but only if dark mode is enabled on the system **Windows 11 Only**
|
||||
MicaDark,
|
||||
/// Mica effect with light mode **Windows 11 Only**
|
||||
MicaLight,
|
||||
/// Tabbed effect that matches the system dark preference **Windows 11 Only**
|
||||
Tabbed,
|
||||
/// Tabbed effect with dark mode but only if dark mode is enabled on the system **Windows 11 Only**
|
||||
TabbedDark,
|
||||
/// Tabbed effect with light mode **Windows 11 Only**
|
||||
TabbedLight,
|
||||
/// **Windows 7/10/11(22H1) Only**
|
||||
///
|
||||
/// ## Notes
|
||||
///
|
||||
/// This effect has bad performance when resizing/dragging the window on Windows 11 build 22621.
|
||||
Blur,
|
||||
/// **Windows 10/11 Only**
|
||||
///
|
||||
/// ## Notes
|
||||
///
|
||||
/// This effect has bad performance when resizing/dragging the window on Windows 10 v1903+ and Windows 11 build 22000.
|
||||
Acrylic,
|
||||
}
|
||||
|
||||
/// Window effect state **macOS only**
|
||||
///
|
||||
/// <https://developer.apple.com/documentation/appkit/nsvisualeffectview/state>
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Copy, Deserialize, Serialize)]
|
||||
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub enum WindowEffectState {
|
||||
/// Make window effect state follow the window's active state
|
||||
FollowsWindowActiveState,
|
||||
/// Make window effect state always active
|
||||
Active,
|
||||
/// Make window effect state always inactive
|
||||
Inactive,
|
||||
}
|
||||
}
|
||||
|
||||
pub use window_effects::{WindowEffect, WindowEffectState};
|
||||
|
||||
/// How the window title bar should be displayed on macOS.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Copy, Default)]
|
||||
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
|
||||
#[non_exhaustive]
|
||||
pub enum TitleBarStyle {
|
||||
/// A normal title bar.
|
||||
#[default]
|
||||
Visible,
|
||||
/// Makes the title bar transparent, so the window background color is shown instead.
|
||||
///
|
||||
/// Useful if you don't need to have actual HTML under the title bar. This lets you avoid the caveats of using `TitleBarStyle::Overlay`. Will be more useful when Tauri lets you set a custom window background color.
|
||||
Transparent,
|
||||
/// Shows the title bar as a transparent overlay over the window's content.
|
||||
///
|
||||
/// Keep in mind:
|
||||
/// - The height of the title bar is different on different OS versions, which can lead to window the controls and title not being where you don't expect.
|
||||
/// - You need to define a custom drag region to make your window draggable, however due to a limitation you can't drag the window when it's not in focus <https://github.com/tauri-apps/tauri/issues/4316>.
|
||||
/// - The color of the window title depends on the system theme.
|
||||
Overlay,
|
||||
}
|
||||
|
||||
impl Serialize for TitleBarStyle {
|
||||
fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
serializer.serialize_str(self.to_string().as_ref())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for TitleBarStyle {
|
||||
fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
let s = String::deserialize(deserializer)?;
|
||||
Ok(match s.to_lowercase().as_str() {
|
||||
"transparent" => Self::Transparent,
|
||||
"overlay" => Self::Overlay,
|
||||
_ => Self::Visible,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for TitleBarStyle {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"{}",
|
||||
match self {
|
||||
Self::Visible => "Visible",
|
||||
Self::Transparent => "Transparent",
|
||||
Self::Overlay => "Overlay",
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// System theme.
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
|
||||
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
|
||||
#[non_exhaustive]
|
||||
pub enum Theme {
|
||||
/// Light theme.
|
||||
Light,
|
||||
/// Dark theme.
|
||||
Dark,
|
||||
}
|
||||
|
||||
impl Serialize for Theme {
|
||||
fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
serializer.serialize_str(self.to_string().as_ref())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for Theme {
|
||||
fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
let s = String::deserialize(deserializer)?;
|
||||
Ok(match s.to_lowercase().as_str() {
|
||||
"dark" => Self::Dark,
|
||||
_ => Self::Light,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for Theme {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"{}",
|
||||
match self {
|
||||
Self::Light => "light",
|
||||
Self::Dark => "dark",
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Information about environment variables.
|
||||
#[derive(Debug, Clone)]
|
||||
#[non_exhaustive]
|
||||
pub struct Env {
|
||||
/// The APPIMAGE environment variable.
|
||||
#[cfg(target_os = "linux")]
|
||||
pub appimage: Option<std::ffi::OsString>,
|
||||
/// The APPDIR environment variable.
|
||||
#[cfg(target_os = "linux")]
|
||||
pub appdir: Option<std::ffi::OsString>,
|
||||
/// The command line arguments of the current process.
|
||||
pub args_os: Vec<OsString>,
|
||||
}
|
||||
|
||||
#[allow(clippy::derivable_impls)]
|
||||
impl Default for Env {
|
||||
fn default() -> Self {
|
||||
let args_os = std::env::args_os().collect();
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
let env = Self {
|
||||
#[cfg(target_os = "linux")]
|
||||
appimage: std::env::var_os("APPIMAGE"),
|
||||
#[cfg(target_os = "linux")]
|
||||
appdir: std::env::var_os("APPDIR"),
|
||||
args_os,
|
||||
};
|
||||
if env.appimage.is_some() || env.appdir.is_some() {
|
||||
// validate that we're actually running on an AppImage
|
||||
// an AppImage is mounted to `/$TEMPDIR/.mount_${appPrefix}${hash}`
|
||||
// see <https://github.com/AppImage/AppImageKit/blob/1681fd84dbe09c7d9b22e13cdb16ea601aa0ec47/src/runtime.c#L501>
|
||||
// note that it is safe to use `std::env::current_exe` here since we just loaded an AppImage.
|
||||
let is_temp = std::env::current_exe()
|
||||
.map(|p| {
|
||||
p.display()
|
||||
.to_string()
|
||||
.starts_with(&format!("{}/.mount_", std::env::temp_dir().display()))
|
||||
})
|
||||
.unwrap_or(true);
|
||||
|
||||
if !is_temp {
|
||||
log::warn!(
|
||||
"`APPDIR` or `APPIMAGE` environment variable found but this application was not detected as an AppImage; this might be a security issue."
|
||||
);
|
||||
}
|
||||
}
|
||||
env
|
||||
}
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
{
|
||||
Self { args_os }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The result type of `tauri-utils`.
|
||||
pub type Result<T> = std::result::Result<T, Error>;
|
||||
|
||||
/// The error type of `tauri-utils`.
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
#[non_exhaustive]
|
||||
pub enum Error {
|
||||
/// Target triple architecture error
|
||||
#[error("Unable to determine target-architecture")]
|
||||
Architecture,
|
||||
/// Target triple OS error
|
||||
#[error("Unable to determine target-os")]
|
||||
Os,
|
||||
/// Target triple environment error
|
||||
#[error("Unable to determine target-environment")]
|
||||
Environment,
|
||||
/// Tried to get resource on an unsupported platform
|
||||
#[error("Unsupported platform for reading resources")]
|
||||
UnsupportedPlatform,
|
||||
/// Get parent process error
|
||||
#[error("Could not get parent process")]
|
||||
ParentProcess,
|
||||
/// Get parent process PID error
|
||||
#[error("Could not get parent PID")]
|
||||
ParentPid,
|
||||
/// Get child process error
|
||||
#[error("Could not get child process")]
|
||||
ChildProcess,
|
||||
/// IO error
|
||||
#[error("{0}")]
|
||||
Io(#[from] std::io::Error),
|
||||
/// Invalid pattern.
|
||||
#[error("invalid pattern `{0}`. Expected either `brownfield` or `isolation`.")]
|
||||
InvalidPattern(String),
|
||||
/// Invalid glob pattern.
|
||||
#[cfg(feature = "resources")]
|
||||
#[error("{0}")]
|
||||
GlobPattern(#[from] glob::PatternError),
|
||||
/// Failed to use glob pattern.
|
||||
#[cfg(feature = "resources")]
|
||||
#[error("`{0}`")]
|
||||
Glob(#[from] glob::GlobError),
|
||||
/// Glob pattern did not find any results.
|
||||
#[cfg(feature = "resources")]
|
||||
#[error("glob pattern {0} path not found or didn't match any files.")]
|
||||
GlobPathNotFound(String),
|
||||
/// Error walking directory.
|
||||
#[cfg(feature = "resources")]
|
||||
#[error("{0}")]
|
||||
WalkdirError(#[from] walkdir::Error),
|
||||
/// Not allowed to walk dir.
|
||||
#[cfg(feature = "resources")]
|
||||
#[error(
|
||||
"could not walk directory `{0}`, try changing `allow_walk` to true on the `ResourcePaths` constructor."
|
||||
)]
|
||||
NotAllowedToWalkDir(std::path::PathBuf),
|
||||
/// Resource path doesn't exist
|
||||
#[cfg(feature = "resources")]
|
||||
#[error("resource path `{0}` doesn't exist")]
|
||||
ResourcePathNotFound(std::path::PathBuf),
|
||||
}
|
||||
|
||||
/// Reconstructs a path from its components using the platform separator then converts it to String and removes UNC prefixes on Windows if it exists.
|
||||
pub fn display_path<P: AsRef<Path>>(p: P) -> String {
|
||||
dunce::simplified(&p.as_ref().components().collect::<PathBuf>())
|
||||
.display()
|
||||
.to_string()
|
||||
}
|
||||
|
||||
/// Write the file only if the content of the existing file (if any) is different.
|
||||
///
|
||||
/// This will always write unless the file exists with identical content.
|
||||
pub fn write_if_changed<P, C>(path: P, content: C) -> std::io::Result<()>
|
||||
where
|
||||
P: AsRef<Path>,
|
||||
C: AsRef<[u8]>,
|
||||
{
|
||||
if let Ok(existing) = std::fs::read(&path)
|
||||
&& existing == content.as_ref()
|
||||
{
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
std::fs::write(path, content)
|
||||
}
|
||||
154
apps/tauri/src-tauri/vendor/tauri-utils/src/mime_type.rs
vendored
Normal file
154
apps/tauri/src-tauri/vendor/tauri-utils/src/mime_type.rs
vendored
Normal file
|
|
@ -0,0 +1,154 @@
|
|||
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
//! Determine a mime type from a URI or file contents.
|
||||
|
||||
use std::fmt;
|
||||
|
||||
const MIMETYPE_PLAIN: &str = "text/plain";
|
||||
|
||||
/// [Web Compatible MimeTypes](https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types#important_mime_types_for_web_developers)
|
||||
#[allow(missing_docs)]
|
||||
pub enum MimeType {
|
||||
Css,
|
||||
Csv,
|
||||
Html,
|
||||
Ico,
|
||||
Js,
|
||||
Json,
|
||||
Jsonld,
|
||||
Mp4,
|
||||
OctetStream,
|
||||
Rtf,
|
||||
Svg,
|
||||
Txt,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for MimeType {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
let mime = match self {
|
||||
MimeType::Css => "text/css",
|
||||
MimeType::Csv => "text/csv",
|
||||
MimeType::Html => "text/html",
|
||||
MimeType::Ico => "image/vnd.microsoft.icon",
|
||||
MimeType::Js => "text/javascript",
|
||||
MimeType::Json => "application/json",
|
||||
MimeType::Jsonld => "application/ld+json",
|
||||
MimeType::Mp4 => "video/mp4",
|
||||
MimeType::OctetStream => "application/octet-stream",
|
||||
MimeType::Rtf => "application/rtf",
|
||||
MimeType::Svg => "image/svg+xml",
|
||||
MimeType::Txt => MIMETYPE_PLAIN,
|
||||
};
|
||||
write!(f, "{mime}")
|
||||
}
|
||||
}
|
||||
|
||||
impl MimeType {
|
||||
/// parse a URI suffix to convert text/plain mimeType to their actual web compatible mimeType.
|
||||
pub fn parse_from_uri(uri: &str) -> MimeType {
|
||||
Self::parse_from_uri_with_fallback(uri, Self::Html)
|
||||
}
|
||||
|
||||
/// parse a URI suffix to convert text/plain mimeType to their actual web compatible mimeType with specified fallback for unknown file extensions.
|
||||
pub fn parse_from_uri_with_fallback(uri: &str, fallback: MimeType) -> MimeType {
|
||||
let suffix = uri.split('.').next_back();
|
||||
match suffix {
|
||||
Some("bin") => Self::OctetStream,
|
||||
Some("css" | "less" | "sass" | "styl") => Self::Css,
|
||||
Some("csv") => Self::Csv,
|
||||
Some("html") => Self::Html,
|
||||
Some("ico") => Self::Ico,
|
||||
Some("js") => Self::Js,
|
||||
Some("json") => Self::Json,
|
||||
Some("jsonld") => Self::Jsonld,
|
||||
Some("mjs") => Self::Js,
|
||||
Some("mp4") => Self::Mp4,
|
||||
Some("rtf") => Self::Rtf,
|
||||
Some("svg") => Self::Svg,
|
||||
Some("txt") => Self::Txt,
|
||||
// Assume HTML when a TLD is found for eg. `wry:://tauri.app` | `wry://hello.com`
|
||||
Some(_) => fallback,
|
||||
// using octet stream according to this:
|
||||
// <https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types/Common_types>
|
||||
None => Self::OctetStream,
|
||||
}
|
||||
}
|
||||
|
||||
/// infer mimetype from content (or) URI if needed.
|
||||
pub fn parse(content: &[u8], uri: &str) -> String {
|
||||
Self::parse_with_fallback(content, uri, Self::Html)
|
||||
}
|
||||
/// infer mimetype from content (or) URI if needed with specified fallback for unknown file extensions.
|
||||
pub fn parse_with_fallback(content: &[u8], uri: &str, fallback: MimeType) -> String {
|
||||
let mime = if uri.ends_with(".svg") {
|
||||
// when reading svg, we can't use `infer`
|
||||
None
|
||||
} else {
|
||||
infer::get(content).map(|info| info.mime_type())
|
||||
};
|
||||
|
||||
match mime {
|
||||
Some(mime) if mime == MIMETYPE_PLAIN => {
|
||||
Self::parse_from_uri_with_fallback(uri, fallback).to_string()
|
||||
}
|
||||
None => Self::parse_from_uri_with_fallback(uri, fallback).to_string(),
|
||||
Some(mime) => mime.to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn should_parse_mimetype_from_uri() {
|
||||
let css = MimeType::parse_from_uri(
|
||||
"https://unpkg.com/browse/bootstrap@4.1.0/dist/css/bootstrap-grid.css",
|
||||
)
|
||||
.to_string();
|
||||
assert_eq!(css, "text/css".to_string());
|
||||
|
||||
let csv: String = MimeType::parse_from_uri("https://example.com/random.csv").to_string();
|
||||
assert_eq!(csv, "text/csv".to_string());
|
||||
|
||||
let ico: String =
|
||||
MimeType::parse_from_uri("https://icons.duckduckgo.com/ip3/microsoft.com.ico").to_string();
|
||||
assert_eq!(ico, String::from("image/vnd.microsoft.icon"));
|
||||
|
||||
let html: String = MimeType::parse_from_uri("https://tauri.app/index.html").to_string();
|
||||
assert_eq!(html, String::from("text/html"));
|
||||
|
||||
let js: String =
|
||||
MimeType::parse_from_uri("https://unpkg.com/react@17.0.1/umd/react.production.min.js")
|
||||
.to_string();
|
||||
assert_eq!(js, "text/javascript".to_string());
|
||||
|
||||
let json: String =
|
||||
MimeType::parse_from_uri("https://unpkg.com/browse/react@17.0.1/build-info.json").to_string();
|
||||
assert_eq!(json, String::from("application/json"));
|
||||
|
||||
let jsonld: String = MimeType::parse_from_uri("https:/example.com/hello.jsonld").to_string();
|
||||
assert_eq!(jsonld, String::from("application/ld+json"));
|
||||
|
||||
let mjs: String = MimeType::parse_from_uri("https://example.com/bundled.mjs").to_string();
|
||||
assert_eq!(mjs, String::from("text/javascript"));
|
||||
|
||||
let mp4: String = MimeType::parse_from_uri("https://example.com/video.mp4").to_string();
|
||||
assert_eq!(mp4, String::from("video/mp4"));
|
||||
|
||||
let rtf: String = MimeType::parse_from_uri("https://example.com/document.rtf").to_string();
|
||||
assert_eq!(rtf, String::from("application/rtf"));
|
||||
|
||||
let svg: String = MimeType::parse_from_uri("https://example.com/picture.svg").to_string();
|
||||
assert_eq!(svg, String::from("image/svg+xml"));
|
||||
|
||||
let txt: String = MimeType::parse_from_uri("https://example.com/file.txt").to_string();
|
||||
assert_eq!(txt, String::from("text/plain"));
|
||||
|
||||
let custom_scheme = MimeType::parse_from_uri("wry://tauri.app").to_string();
|
||||
assert_eq!(custom_scheme, String::from("text/html"));
|
||||
}
|
||||
}
|
||||
154
apps/tauri/src-tauri/vendor/tauri-utils/src/pattern/isolation.js
vendored
Normal file
154
apps/tauri/src-tauri/vendor/tauri-utils/src/pattern/isolation.js
vendored
Normal file
|
|
@ -0,0 +1,154 @@
|
|||
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
/**
|
||||
* IMPORTANT: See ipc.js for the main frame implementation.
|
||||
* main frame -> isolation frame = isolation payload
|
||||
* isolation frame -> main frame = isolation message
|
||||
*/
|
||||
|
||||
;(async function () {
|
||||
/**
|
||||
* Sends the message to the isolation frame.
|
||||
* @param {any} message
|
||||
*/
|
||||
function sendMessage(message) {
|
||||
window.parent.postMessage(message, '*')
|
||||
}
|
||||
|
||||
/**
|
||||
* @type {string} - The main frame origin.
|
||||
*/
|
||||
const origin = __TEMPLATE_origin__
|
||||
|
||||
/**
|
||||
* @type {Uint8Array} - Injected by Tauri during runtime
|
||||
*/
|
||||
const aesGcmKeyRaw = new Uint8Array(__TEMPLATE_runtime_aes_gcm_key__)
|
||||
|
||||
/**
|
||||
* @type {CryptoKey}
|
||||
*/
|
||||
const aesGcmKey = await window.crypto.subtle.importKey(
|
||||
'raw',
|
||||
aesGcmKeyRaw,
|
||||
'AES-GCM',
|
||||
false,
|
||||
['encrypt']
|
||||
)
|
||||
|
||||
/**
|
||||
* @param {object} data
|
||||
* @return {Promise<{nonce: number[], payload: number[]}>}
|
||||
*/
|
||||
async function encrypt(payload) {
|
||||
const algorithm = Object.create(null)
|
||||
algorithm.name = 'AES-GCM'
|
||||
algorithm.iv = window.crypto.getRandomValues(new Uint8Array(12))
|
||||
|
||||
const { contentType, data } = __RAW_process_ipc_message_fn__(payload)
|
||||
|
||||
const message =
|
||||
typeof data === 'string'
|
||||
? new TextEncoder().encode(data)
|
||||
: ArrayBuffer.isView(data) || data instanceof ArrayBuffer
|
||||
? data
|
||||
: new Uint8Array(data)
|
||||
|
||||
return window.crypto.subtle
|
||||
.encrypt(algorithm, aesGcmKey, message)
|
||||
.then((payload) => {
|
||||
const result = Object.create(null)
|
||||
result.nonce = Array.from(new Uint8Array(algorithm.iv))
|
||||
result.payload = Array.from(new Uint8Array(payload))
|
||||
result.contentType = contentType
|
||||
return result
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Detects if a message event is a valid isolation message.
|
||||
*
|
||||
* @param {MessageEvent<object>} event - a message event that is expected to be an isolation message
|
||||
* @return {boolean} - if the event was a valid isolation message
|
||||
*/
|
||||
function isIsolationMessage(data) {
|
||||
if (typeof data === 'object' && typeof data.payload === 'object') {
|
||||
const keys = data.payload ? Object.keys(data.payload) : []
|
||||
return (
|
||||
keys.length > 0
|
||||
&& keys.every(
|
||||
(key) => key === 'nonce' || key === 'payload' || key === 'contentType'
|
||||
)
|
||||
)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect if a message event is a valid isolation payload.
|
||||
*
|
||||
* @param {MessageEvent<object>} event - a message event that is expected to be an isolation payload
|
||||
* @return boolean
|
||||
*/
|
||||
function isIsolationPayload(data) {
|
||||
return (
|
||||
typeof data === 'object'
|
||||
&& 'callback' in data
|
||||
&& 'error' in data
|
||||
&& !isIsolationMessage(data)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle incoming payload events.
|
||||
* @param {MessageEvent<any>} event
|
||||
*/
|
||||
async function payloadHandler(event) {
|
||||
if (event.origin !== origin || !isIsolationPayload(event.data)) {
|
||||
return
|
||||
}
|
||||
|
||||
let data = event.data
|
||||
|
||||
if (typeof window.__TAURI_ISOLATION_HOOK__ === 'function') {
|
||||
// await even if it's not async so that we can support async ones
|
||||
data = await window.__TAURI_ISOLATION_HOOK__(data)
|
||||
}
|
||||
|
||||
const message = Object.create(null)
|
||||
message.cmd = data.cmd
|
||||
message.callback = data.callback
|
||||
message.error = data.error
|
||||
message.options = data.options
|
||||
message.payload = await encrypt(data.payload)
|
||||
sendMessage(message)
|
||||
}
|
||||
|
||||
window.addEventListener('message', payloadHandler, false)
|
||||
|
||||
/**
|
||||
* @type {number} - How many milliseconds to wait between ready checks
|
||||
*/
|
||||
const readyIntervalMs = 50
|
||||
|
||||
/**
|
||||
* Wait until this Isolation context is ready to receive messages, and let the main frame know.
|
||||
*/
|
||||
function waitUntilReady() {
|
||||
// consider either a function or an explicitly set null value as the ready signal
|
||||
if (
|
||||
typeof window.__TAURI_ISOLATION_HOOK__ === 'function'
|
||||
|| window.__TAURI_ISOLATION_HOOK__ === null
|
||||
) {
|
||||
sendMessage('__TAURI_ISOLATION_READY__')
|
||||
} else {
|
||||
setTimeout(waitUntilReady, readyIntervalMs)
|
||||
}
|
||||
}
|
||||
|
||||
setTimeout(waitUntilReady, readyIntervalMs)
|
||||
})()
|
||||
|
||||
document.currentScript?.remove()
|
||||
171
apps/tauri/src-tauri/vendor/tauri-utils/src/pattern/isolation.rs
vendored
Normal file
171
apps/tauri/src-tauri/vendor/tauri-utils/src/pattern/isolation.rs
vendored
Normal file
|
|
@ -0,0 +1,171 @@
|
|||
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
use std::array::TryFromSliceError;
|
||||
use std::borrow::Cow;
|
||||
use std::fmt::{Debug, Formatter};
|
||||
use std::string::FromUtf8Error;
|
||||
|
||||
use aes_gcm::aead::Aead;
|
||||
use aes_gcm::{Aes256Gcm, KeyInit, Nonce};
|
||||
use getrandom::Error as CsprngError;
|
||||
use serialize_to_javascript::{Template, default_template};
|
||||
|
||||
/// The style for the isolation iframe.
|
||||
pub const IFRAME_STYLE: &str = "#__tauri_isolation__ { display: none !important }";
|
||||
|
||||
/// Errors that can occur during Isolation keys generation.
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
#[non_exhaustive]
|
||||
pub enum Error {
|
||||
/// Something went wrong with the CSPRNG.
|
||||
#[error("CSPRNG error")]
|
||||
Csprng(#[from] CsprngError),
|
||||
|
||||
/// Something went wrong with decrypting an AES-GCM payload
|
||||
#[error("AES-GCM")]
|
||||
Aes,
|
||||
|
||||
/// Nonce was not 96 bits
|
||||
#[error("Nonce: {0}")]
|
||||
NonceSize(#[from] TryFromSliceError),
|
||||
|
||||
/// Payload was not valid utf8
|
||||
#[error("{0}")]
|
||||
Utf8(#[from] FromUtf8Error),
|
||||
|
||||
/// Invalid json format
|
||||
#[error("{0}")]
|
||||
Json(#[from] serde_json::Error),
|
||||
}
|
||||
|
||||
/// A formatted AES-GCM cipher instance along with the key used to initialize it.
|
||||
#[derive(Clone)]
|
||||
pub struct AesGcmPair {
|
||||
raw: [u8; 32],
|
||||
key: Aes256Gcm,
|
||||
}
|
||||
|
||||
impl Debug for AesGcmPair {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "AesGcmPair(...)")
|
||||
}
|
||||
}
|
||||
|
||||
impl AesGcmPair {
|
||||
fn new() -> Result<Self, Error> {
|
||||
let mut raw = [0u8; 32];
|
||||
getrandom::fill(&mut raw)?;
|
||||
let key = aes_gcm::Key::<Aes256Gcm>::from_slice(&raw);
|
||||
Ok(Self {
|
||||
raw,
|
||||
key: Aes256Gcm::new(key),
|
||||
})
|
||||
}
|
||||
|
||||
/// The raw value used to create the AES-GCM key
|
||||
pub fn raw(&self) -> &[u8; 32] {
|
||||
&self.raw
|
||||
}
|
||||
|
||||
/// The formatted AES-GCM key
|
||||
pub fn key(&self) -> &Aes256Gcm {
|
||||
&self.key
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
pub fn encrypt(&self, nonce: &[u8; 12], payload: &[u8]) -> Result<Vec<u8>, Error> {
|
||||
self
|
||||
.key
|
||||
.encrypt(nonce.into(), payload)
|
||||
.map_err(|_| self::Error::Aes)
|
||||
}
|
||||
}
|
||||
|
||||
/// All cryptographic keys required for Isolation encryption
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Keys {
|
||||
/// AES-GCM key
|
||||
aes_gcm: AesGcmPair,
|
||||
}
|
||||
|
||||
impl Keys {
|
||||
/// Securely generate required keys for Isolation encryption.
|
||||
pub fn new() -> Result<Self, Error> {
|
||||
AesGcmPair::new().map(|aes_gcm| Self { aes_gcm })
|
||||
}
|
||||
|
||||
/// The AES-GCM data (and raw data).
|
||||
pub fn aes_gcm(&self) -> &AesGcmPair {
|
||||
&self.aes_gcm
|
||||
}
|
||||
|
||||
/// Decrypts a message using the generated keys.
|
||||
pub fn decrypt(&self, raw: RawIsolationPayload<'_>) -> Result<Vec<u8>, Error> {
|
||||
let RawIsolationPayload { nonce, payload, .. } = raw;
|
||||
let nonce: [u8; 12] = nonce.as_ref().try_into()?;
|
||||
self
|
||||
.aes_gcm
|
||||
.key
|
||||
.decrypt(Nonce::from_slice(&nonce), payload.as_ref())
|
||||
.map_err(|_| self::Error::Aes)
|
||||
}
|
||||
}
|
||||
|
||||
/// Raw representation of
|
||||
#[derive(Debug, serde::Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct RawIsolationPayload<'a> {
|
||||
nonce: Cow<'a, [u8]>,
|
||||
payload: Cow<'a, [u8]>,
|
||||
content_type: Cow<'a, str>,
|
||||
}
|
||||
|
||||
impl<'a> RawIsolationPayload<'a> {
|
||||
/// Content type of this payload.
|
||||
pub fn content_type(&self) -> &Cow<'a, str> {
|
||||
&self.content_type
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> TryFrom<&'a Vec<u8>> for RawIsolationPayload<'a> {
|
||||
type Error = Error;
|
||||
|
||||
fn try_from(value: &'a Vec<u8>) -> Result<Self, Self::Error> {
|
||||
serde_json::from_slice(value).map_err(Into::into)
|
||||
}
|
||||
}
|
||||
|
||||
/// The Isolation JavaScript template meant to be injected during codegen.
|
||||
///
|
||||
/// Note: This struct is not considered part of the stable API
|
||||
#[derive(Template)]
|
||||
#[default_template("isolation.js")]
|
||||
pub struct IsolationJavascriptCodegen {
|
||||
// this template intentionally does not include the runtime field
|
||||
}
|
||||
|
||||
/// The Isolation JavaScript template meant to be injected during runtime.
|
||||
///
|
||||
/// Note: This struct is not considered part of the stable API
|
||||
#[derive(Template)]
|
||||
#[default_template("isolation.js")]
|
||||
pub struct IsolationJavascriptRuntime<'a> {
|
||||
/// The key used on the Rust backend and the Isolation Javascript
|
||||
pub runtime_aes_gcm_key: &'a [u8; 32],
|
||||
/// The origin the isolation application is expecting messages from.
|
||||
pub origin: String,
|
||||
/// The function that processes the IPC message.
|
||||
#[raw]
|
||||
pub process_ipc_message_fn: &'a str,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
#[test]
|
||||
fn create_keys() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let _ = super::Keys::new()?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
7
apps/tauri/src-tauri/vendor/tauri-utils/src/pattern/mod.rs
vendored
Normal file
7
apps/tauri/src-tauri/vendor/tauri-utils/src/pattern/mod.rs
vendored
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
/// Handling the Tauri "Isolation" Pattern.
|
||||
#[cfg(feature = "isolation")]
|
||||
pub mod isolation;
|
||||
434
apps/tauri/src-tauri/vendor/tauri-utils/src/platform.rs
vendored
Normal file
434
apps/tauri/src-tauri/vendor/tauri-utils/src/platform.rs
vendored
Normal file
|
|
@ -0,0 +1,434 @@
|
|||
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
//! Platform helper functions.
|
||||
|
||||
use std::{fmt::Display, path::PathBuf};
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::{Env, PackageInfo, config::BundleType};
|
||||
|
||||
mod starting_binary;
|
||||
|
||||
/// URI prefix of a Tauri asset.
|
||||
///
|
||||
/// This is referenced in the Tauri Android library,
|
||||
/// which resolves these assets to a file descriptor.
|
||||
#[cfg(target_os = "android")]
|
||||
pub const ANDROID_ASSET_PROTOCOL_URI_PREFIX: &str = "asset://localhost/";
|
||||
|
||||
/// Platform target.
|
||||
#[derive(PartialEq, Eq, Copy, Debug, Clone, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[non_exhaustive]
|
||||
pub enum Target {
|
||||
/// MacOS.
|
||||
#[serde(rename = "macOS")]
|
||||
MacOS,
|
||||
/// Windows.
|
||||
Windows,
|
||||
/// Linux.
|
||||
Linux,
|
||||
/// Android.
|
||||
Android,
|
||||
/// iOS.
|
||||
#[serde(rename = "iOS")]
|
||||
Ios,
|
||||
}
|
||||
|
||||
impl Display for Target {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"{}",
|
||||
match self {
|
||||
Self::MacOS => "macOS",
|
||||
Self::Windows => "windows",
|
||||
Self::Linux => "linux",
|
||||
Self::Android => "android",
|
||||
Self::Ios => "iOS",
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl Target {
|
||||
/// Parses the target from the given target triple.
|
||||
pub fn from_triple(target: &str) -> Self {
|
||||
if target.contains("darwin") {
|
||||
Self::MacOS
|
||||
} else if target.contains("windows") {
|
||||
Self::Windows
|
||||
} else if target.contains("android") {
|
||||
Self::Android
|
||||
} else if target.contains("ios") {
|
||||
Self::Ios
|
||||
} else {
|
||||
Self::Linux
|
||||
}
|
||||
}
|
||||
|
||||
/// Gets the current build target.
|
||||
pub fn current() -> Self {
|
||||
if cfg!(target_os = "macos") {
|
||||
Self::MacOS
|
||||
} else if cfg!(target_os = "windows") {
|
||||
Self::Windows
|
||||
} else if cfg!(target_os = "ios") {
|
||||
Self::Ios
|
||||
} else if cfg!(target_os = "android") {
|
||||
Self::Android
|
||||
} else {
|
||||
Self::Linux
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether the target is mobile or not.
|
||||
pub fn is_mobile(&self) -> bool {
|
||||
matches!(self, Target::Android | Target::Ios)
|
||||
}
|
||||
|
||||
/// Whether the target is desktop or not.
|
||||
pub fn is_desktop(&self) -> bool {
|
||||
!self.is_mobile()
|
||||
}
|
||||
}
|
||||
|
||||
/// Retrieves the currently running binary's path, taking into account security considerations.
|
||||
///
|
||||
/// The path is cached as soon as possible (before even `main` runs) and that value is returned
|
||||
/// repeatedly instead of fetching the path every time. It is possible for the path to not be found,
|
||||
/// or explicitly disabled (see following macOS specific behavior).
|
||||
///
|
||||
/// # Platform-specific behavior
|
||||
///
|
||||
/// On `macOS`, this function will return an error if the original path contained any symlinks
|
||||
/// due to less protection on macOS regarding symlinks. This behavior can be disabled by setting the
|
||||
/// `process-relaunch-dangerous-allow-symlink-macos` feature, although it is *highly discouraged*.
|
||||
///
|
||||
/// # Security
|
||||
///
|
||||
/// If the above platform-specific behavior does **not** take place, this function uses the
|
||||
/// following resolution.
|
||||
///
|
||||
/// We canonicalize the path we received from [`std::env::current_exe`] to resolve any soft links.
|
||||
/// This avoids the usual issue of needing the file to exist at the passed path because a valid
|
||||
/// current executable result for our purpose should always exist. Notably,
|
||||
/// [`std::env::current_exe`] also has a security section that goes over a theoretical attack using
|
||||
/// hard links. Let's cover some specific topics that relate to different ways an attacker might
|
||||
/// try to trick this function into returning the wrong binary path.
|
||||
///
|
||||
/// ## Symlinks ("Soft Links")
|
||||
///
|
||||
/// [`std::path::Path::canonicalize`] is used to resolve symbolic links to the original path,
|
||||
/// including nested symbolic links (`link2 -> link1 -> bin`). On macOS, any results that include
|
||||
/// a symlink are rejected by default due to lesser symlink protections. This can be disabled,
|
||||
/// **although discouraged**, with the `process-relaunch-dangerous-allow-symlink-macos` feature.
|
||||
///
|
||||
/// ## Hard Links
|
||||
///
|
||||
/// A [Hard Link] is a named entry that points to a file in the file system.
|
||||
/// On most systems, this is what you would think of as a "file". The term is
|
||||
/// used on filesystems that allow multiple entries to point to the same file.
|
||||
/// The linked [Hard Link] Wikipedia page provides a decent overview.
|
||||
///
|
||||
/// In short, unless the attacker was able to create the link with elevated
|
||||
/// permissions, it should generally not be possible for them to hard link
|
||||
/// to a file they do not have permissions to - with exception to possible
|
||||
/// operating system exploits.
|
||||
///
|
||||
/// There are also some platform-specific information about this below.
|
||||
///
|
||||
/// ### Windows
|
||||
///
|
||||
/// Windows requires a permission to be set for the user to create a symlink
|
||||
/// or a hard link, regardless of ownership status of the target. Elevated
|
||||
/// permissions users have the ability to create them.
|
||||
///
|
||||
/// ### macOS
|
||||
///
|
||||
/// macOS allows for the creation of symlinks and hard links to any file.
|
||||
/// Accessing through those links will fail if the user who owns the links
|
||||
/// does not have the proper permissions on the original file.
|
||||
///
|
||||
/// ### Linux
|
||||
///
|
||||
/// Linux allows for the creation of symlinks to any file. Accessing the
|
||||
/// symlink will fail if the user who owns the symlink does not have the
|
||||
/// proper permissions on the original file.
|
||||
///
|
||||
/// Linux additionally provides a kernel hardening feature since version
|
||||
/// 3.6 (30 September 2012). Most distributions since then have enabled
|
||||
/// the protection (setting `fs.protected_hardlinks = 1`) by default, which
|
||||
/// means that a vast majority of desktop Linux users should have it enabled.
|
||||
/// **The feature prevents the creation of hardlinks that the user does not own
|
||||
/// or have read/write access to.** [See the patch that enabled this].
|
||||
///
|
||||
/// [Hard Link]: https://en.wikipedia.org/wiki/Hard_link
|
||||
/// [See the patch that enabled this]: https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=800179c9b8a1e796e441674776d11cd4c05d61d7
|
||||
pub fn current_exe() -> std::io::Result<PathBuf> {
|
||||
self::starting_binary::STARTING_BINARY.cloned()
|
||||
}
|
||||
|
||||
/// Try to determine the current target triple.
|
||||
///
|
||||
/// Returns a target triple (e.g. `x86_64-unknown-linux-gnu` or `i686-pc-windows-msvc`) or an
|
||||
/// `Error::Config` if the current config cannot be determined or is not some combination of the
|
||||
/// following values:
|
||||
/// `linux, mac, windows` -- `i686, x86, armv7` -- `gnu, musl, msvc`
|
||||
///
|
||||
/// * Errors:
|
||||
/// * Unexpected system config
|
||||
pub fn target_triple() -> crate::Result<String> {
|
||||
let arch = if cfg!(target_arch = "x86") {
|
||||
"i686"
|
||||
} else if cfg!(target_arch = "x86_64") {
|
||||
"x86_64"
|
||||
} else if cfg!(target_arch = "arm") {
|
||||
"armv7"
|
||||
} else if cfg!(target_arch = "aarch64") {
|
||||
"aarch64"
|
||||
} else if cfg!(target_arch = "riscv64") {
|
||||
"riscv64"
|
||||
} else {
|
||||
return Err(crate::Error::Architecture);
|
||||
};
|
||||
|
||||
let os = if cfg!(target_os = "linux") {
|
||||
"unknown-linux"
|
||||
} else if cfg!(target_os = "macos") {
|
||||
"apple-darwin"
|
||||
} else if cfg!(target_os = "windows") {
|
||||
"pc-windows"
|
||||
} else if cfg!(target_os = "freebsd") {
|
||||
"unknown-freebsd"
|
||||
} else {
|
||||
return Err(crate::Error::Os);
|
||||
};
|
||||
|
||||
let os = if cfg!(target_os = "macos") || cfg!(target_os = "freebsd") {
|
||||
String::from(os)
|
||||
} else {
|
||||
let env = if cfg!(target_env = "gnu") {
|
||||
"gnu"
|
||||
} else if cfg!(target_env = "musl") {
|
||||
"musl"
|
||||
} else if cfg!(target_env = "msvc") {
|
||||
"msvc"
|
||||
} else {
|
||||
return Err(crate::Error::Environment);
|
||||
};
|
||||
|
||||
format!("{os}-{env}")
|
||||
};
|
||||
|
||||
Ok(format!("{arch}-{os}"))
|
||||
}
|
||||
|
||||
#[cfg(all(not(test), not(target_os = "android")))]
|
||||
fn is_cargo_output_directory(path: &std::path::Path) -> bool {
|
||||
path.join(".cargo-lock").exists()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
const CARGO_OUTPUT_DIRECTORIES: &[&str] = &["debug", "release", "custom-profile"];
|
||||
|
||||
#[cfg(test)]
|
||||
fn is_cargo_output_directory(path: &std::path::Path) -> bool {
|
||||
let Some(last_component) = path.components().next_back() else {
|
||||
return false;
|
||||
};
|
||||
CARGO_OUTPUT_DIRECTORIES
|
||||
.iter()
|
||||
.any(|dirname| &last_component.as_os_str() == dirname)
|
||||
}
|
||||
|
||||
/// Computes the resource directory of the current environment.
|
||||
///
|
||||
/// ## Platform-specific
|
||||
///
|
||||
/// - **Windows:** Resolves to the directory that contains the main executable.
|
||||
/// - **Linux:** When running in an AppImage, the `APPDIR` variable will be set to
|
||||
/// the mounted location of the app, and the resource dir will be `${APPDIR}/usr/lib/${exe_name}`.
|
||||
/// If not running in an AppImage, the path is `/usr/lib/${exe_name}`.
|
||||
/// When running the app from `src-tauri/target/(debug|release)/`, the path is `${exe_dir}/../lib/${exe_name}`.
|
||||
/// - **macOS:** Resolves to `${exe_dir}/../Resources` (inside .app).
|
||||
/// - **iOS:** Resolves to `${exe_dir}/assets`.
|
||||
/// - **Android:** Currently the resources are stored in the APK as assets so it's not a normal file system path,
|
||||
/// we return a special URI prefix `asset://localhost/` here that can be used with the [file system plugin](https://tauri.app/plugin/file-system/),
|
||||
/// with that, you can read the files through [`FsExt::fs`](https://docs.rs/tauri-plugin-fs/latest/tauri_plugin_fs/trait.FsExt.html#tymethod.fs)
|
||||
/// like this: `app.fs().read_to_string(app.path().resource_dir().unwrap().join("resource"));`
|
||||
pub fn resource_dir(package_info: &PackageInfo, env: &Env) -> crate::Result<PathBuf> {
|
||||
#[cfg(target_os = "android")]
|
||||
return resource_dir_android(package_info, env);
|
||||
#[cfg(not(target_os = "android"))]
|
||||
{
|
||||
let exe = current_exe()?;
|
||||
resource_dir_from(exe, package_info, env)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "android")]
|
||||
fn resource_dir_android(_package_info: &PackageInfo, _env: &Env) -> crate::Result<PathBuf> {
|
||||
Ok(PathBuf::from(ANDROID_ASSET_PROTOCOL_URI_PREFIX))
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "android"))]
|
||||
#[allow(unused_variables)]
|
||||
fn resource_dir_from<P: AsRef<std::path::Path>>(
|
||||
exe: P,
|
||||
package_info: &PackageInfo,
|
||||
env: &Env,
|
||||
) -> crate::Result<PathBuf> {
|
||||
let exe_dir = exe.as_ref().parent().expect("failed to get exe directory");
|
||||
let curr_dir = exe_dir.display().to_string();
|
||||
|
||||
let parts: Vec<&str> = curr_dir.split(std::path::MAIN_SEPARATOR).collect();
|
||||
let len = parts.len();
|
||||
|
||||
// Check if running from the Cargo output directory, which means it's an executable in a development machine
|
||||
// We check if the binary is inside a `target` folder which can be either `target/$profile` or `target/$triple/$profile`
|
||||
// and see if there's a .cargo-lock file along the executable
|
||||
// This ensures the check is safer so it doesn't affect apps in production
|
||||
// Windows also includes the resources in the executable folder so we check that too
|
||||
if cfg!(target_os = "windows")
|
||||
|| ((len >= 2 && parts[len - 2] == "target") || (len >= 3 && parts[len - 3] == "target"))
|
||||
&& is_cargo_output_directory(exe_dir)
|
||||
{
|
||||
return Ok(exe_dir.to_path_buf());
|
||||
}
|
||||
|
||||
#[allow(unused_mut, unused_assignments)]
|
||||
let mut res = Err(crate::Error::UnsupportedPlatform);
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
// (canonicalize checks for existence, so there's no need for an extra check)
|
||||
res = if let Ok(bundle_dir) = exe_dir
|
||||
.join(format!("../lib/{}", package_info.name))
|
||||
.canonicalize()
|
||||
{
|
||||
Ok(bundle_dir)
|
||||
} else if let Some(appdir) = &env.appdir {
|
||||
let appdir: &std::path::Path = appdir.as_ref();
|
||||
Ok(PathBuf::from(format!(
|
||||
"{}/usr/lib/{}",
|
||||
appdir.display(),
|
||||
package_info.name
|
||||
)))
|
||||
} else {
|
||||
// running bundle
|
||||
Ok(PathBuf::from(format!("/usr/lib/{}", package_info.name)))
|
||||
};
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
res = exe_dir
|
||||
.join("../Resources")
|
||||
.canonicalize()
|
||||
.map_err(Into::into);
|
||||
}
|
||||
|
||||
#[cfg(target_os = "ios")]
|
||||
{
|
||||
res = exe_dir.join("assets").canonicalize().map_err(Into::into);
|
||||
}
|
||||
|
||||
res
|
||||
}
|
||||
|
||||
// Variable holding the type of bundle the executable is stored in. This is modified by binary
|
||||
// patching during build
|
||||
#[used]
|
||||
// Marked as `mut` because it could get optimized away without it,
|
||||
// see https://github.com/tauri-apps/tauri/pull/13812
|
||||
static mut __TAURI_BUNDLE_TYPE: &str = "__TAURI_BUNDLE_TYPE_VAR_UNK";
|
||||
|
||||
/// Get the type of the bundle current binary is packaged in.
|
||||
/// If the bundle type is unknown, it returns [`Option::None`].
|
||||
pub fn bundle_type() -> Option<BundleType> {
|
||||
unsafe {
|
||||
match __TAURI_BUNDLE_TYPE {
|
||||
"__TAURI_BUNDLE_TYPE_VAR_DEB" => Some(BundleType::Deb),
|
||||
"__TAURI_BUNDLE_TYPE_VAR_RPM" => Some(BundleType::Rpm),
|
||||
"__TAURI_BUNDLE_TYPE_VAR_APP" => Some(BundleType::AppImage),
|
||||
"__TAURI_BUNDLE_TYPE_VAR_MSI" => Some(BundleType::Msi),
|
||||
"__TAURI_BUNDLE_TYPE_VAR_NSS" => Some(BundleType::Nsis),
|
||||
_ => {
|
||||
if cfg!(target_os = "macos") {
|
||||
Some(BundleType::App)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(feature = "build", feature = "build-2"))]
|
||||
mod build {
|
||||
use proc_macro2::TokenStream;
|
||||
use quote::{ToTokens, TokenStreamExt, quote};
|
||||
|
||||
use super::*;
|
||||
|
||||
impl ToTokens for Target {
|
||||
fn to_tokens(&self, tokens: &mut TokenStream) {
|
||||
let prefix = quote! { ::tauri::utils::platform::Target };
|
||||
|
||||
tokens.append_all(match self {
|
||||
Self::MacOS => quote! { #prefix::MacOS },
|
||||
Self::Linux => quote! { #prefix::Linux },
|
||||
Self::Windows => quote! { #prefix::Windows },
|
||||
Self::Android => quote! { #prefix::Android },
|
||||
Self::Ios => quote! { #prefix::Ios },
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::path::PathBuf;
|
||||
|
||||
use crate::{Env, PackageInfo};
|
||||
|
||||
#[test]
|
||||
#[cfg(not(target_os = "android"))]
|
||||
fn resolve_resource_dir() {
|
||||
let package_info = PackageInfo {
|
||||
name: "MyApp".into(),
|
||||
version: "1.0.0".parse().unwrap(),
|
||||
authors: "",
|
||||
description: "",
|
||||
crate_name: "my-app",
|
||||
};
|
||||
let env = Env::default();
|
||||
|
||||
let path = PathBuf::from("/path/to/target/aarch64-apple-darwin/debug/app");
|
||||
let resource_dir = super::resource_dir_from(&path, &package_info, &env).unwrap();
|
||||
assert_eq!(resource_dir, path.parent().unwrap());
|
||||
|
||||
let path = PathBuf::from("/path/to/target/custom-profile/app");
|
||||
let resource_dir = super::resource_dir_from(&path, &package_info, &env).unwrap();
|
||||
assert_eq!(resource_dir, path.parent().unwrap());
|
||||
|
||||
let path = PathBuf::from("/path/to/target/release/app");
|
||||
let resource_dir = super::resource_dir_from(&path, &package_info, &env).unwrap();
|
||||
assert_eq!(resource_dir, path.parent().unwrap());
|
||||
|
||||
let path = PathBuf::from("/path/to/target/unknown-profile/app");
|
||||
#[allow(clippy::needless_borrows_for_generic_args)]
|
||||
let resource_dir = super::resource_dir_from(&path, &package_info, &env);
|
||||
#[cfg(target_os = "macos")]
|
||||
assert!(resource_dir.is_err());
|
||||
#[cfg(target_os = "linux")]
|
||||
assert_eq!(resource_dir.unwrap(), PathBuf::from("/usr/lib/MyApp"));
|
||||
#[cfg(windows)]
|
||||
assert_eq!(resource_dir.unwrap(), path.parent().unwrap());
|
||||
}
|
||||
}
|
||||
83
apps/tauri/src-tauri/vendor/tauri-utils/src/platform/starting_binary.rs
vendored
Normal file
83
apps/tauri/src-tauri/vendor/tauri-utils/src/platform/starting_binary.rs
vendored
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
use ctor::ctor;
|
||||
use std::{
|
||||
io::{Error, ErrorKind, Result},
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
/// A cached version of the current binary using [`ctor`] to cache it before even `main` runs.
|
||||
#[ctor]
|
||||
#[used]
|
||||
pub(super) static STARTING_BINARY: StartingBinary = unsafe { StartingBinary::new() };
|
||||
|
||||
/// Represents a binary path that was cached when the program was loaded.
|
||||
pub(super) struct StartingBinary(std::io::Result<PathBuf>);
|
||||
|
||||
impl StartingBinary {
|
||||
/// Find the starting executable as safely as possible.
|
||||
fn new() -> Self {
|
||||
// see notes on current_exe() for security implications
|
||||
let dangerous_path = match std::env::current_exe() {
|
||||
Ok(dangerous_path) => dangerous_path,
|
||||
error @ Err(_) => return Self(error),
|
||||
};
|
||||
|
||||
// note: this only checks symlinks on problematic platforms, see implementation below
|
||||
if let Some(symlink) = Self::has_symlink(&dangerous_path) {
|
||||
return Self(Err(Error::new(
|
||||
ErrorKind::InvalidData,
|
||||
format!(
|
||||
"StartingBinary found current_exe() that contains a symlink on a non-allowed platform: {}",
|
||||
symlink.display()
|
||||
),
|
||||
)));
|
||||
}
|
||||
|
||||
// we canonicalize the path to resolve any symlinks to the real exe path
|
||||
Self(dangerous_path.canonicalize())
|
||||
}
|
||||
|
||||
/// A clone of the [`PathBuf`] found to be the starting path.
|
||||
///
|
||||
/// Because [`Error`] is not clone-able, it is recreated instead.
|
||||
pub(super) fn cloned(&self) -> Result<PathBuf> {
|
||||
// false positive
|
||||
#[allow(clippy::useless_asref)]
|
||||
self
|
||||
.0
|
||||
.as_ref()
|
||||
.map(Clone::clone)
|
||||
.map_err(|e| Error::new(e.kind(), e.to_string()))
|
||||
}
|
||||
|
||||
/// We only care about checking this on macOS currently, as it has the least symlink protections.
|
||||
#[cfg(any(
|
||||
not(target_os = "macos"),
|
||||
feature = "process-relaunch-dangerous-allow-symlink-macos"
|
||||
))]
|
||||
fn has_symlink(_: &Path) -> Option<&Path> {
|
||||
None
|
||||
}
|
||||
|
||||
/// We only care about checking this on macOS currently, as it has the least symlink protections.
|
||||
#[cfg(all(
|
||||
target_os = "macos",
|
||||
not(feature = "process-relaunch-dangerous-allow-symlink-macos")
|
||||
))]
|
||||
fn has_symlink(path: &Path) -> Option<&Path> {
|
||||
path.ancestors().find(|ancestor| {
|
||||
matches!(
|
||||
ancestor
|
||||
.symlink_metadata()
|
||||
.as_ref()
|
||||
.map(std::fs::Metadata::file_type)
|
||||
.as_ref()
|
||||
.map(std::fs::FileType::is_symlink),
|
||||
Ok(true)
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
88
apps/tauri/src-tauri/vendor/tauri-utils/src/plugin.rs
vendored
Normal file
88
apps/tauri/src-tauri/vendor/tauri-utils/src/plugin.rs
vendored
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
//! Compile-time and runtime types for Tauri plugins.
|
||||
#[cfg(any(feature = "build", feature = "build-2"))]
|
||||
pub use build::*;
|
||||
|
||||
#[cfg(any(feature = "build", feature = "build-2"))]
|
||||
mod build {
|
||||
use std::{
|
||||
env::vars_os,
|
||||
fs,
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
const GLOBAL_API_SCRIPT_PATH_KEY: &str = "GLOBAL_API_SCRIPT_PATH";
|
||||
/// Known file name of the file that contains an array with the path of all API scripts defined with [`define_global_api_script_path`].
|
||||
pub const GLOBAL_API_SCRIPT_FILE_LIST_PATH: &str = "__global-api-script.js";
|
||||
|
||||
/// Defines the path to the global API script using Cargo instructions.
|
||||
pub fn define_global_api_script_path(path: &Path) {
|
||||
println!(
|
||||
"cargo:{GLOBAL_API_SCRIPT_PATH_KEY}={}",
|
||||
path
|
||||
.canonicalize()
|
||||
.expect("failed to canonicalize global API script path")
|
||||
.display()
|
||||
)
|
||||
}
|
||||
|
||||
/// Collects the path of all the global API scripts defined with [`define_global_api_script_path`]
|
||||
/// and saves them to the out dir with filename [`GLOBAL_API_SCRIPT_FILE_LIST_PATH`].
|
||||
///
|
||||
/// `tauri_global_scripts` is only used in Tauri's monorepo for the examples to work
|
||||
/// since they don't have a build script to run `tauri-build` and pull in the deps env vars
|
||||
pub fn save_global_api_scripts_paths(out_dir: &Path, mut tauri_global_scripts: Option<PathBuf>) {
|
||||
let mut scripts = Vec::new();
|
||||
|
||||
for (key, value) in vars_os() {
|
||||
let key = key.to_string_lossy();
|
||||
|
||||
if key == format!("DEP_TAURI_{GLOBAL_API_SCRIPT_PATH_KEY}") {
|
||||
tauri_global_scripts = Some(PathBuf::from(value));
|
||||
} else if key.starts_with("DEP_") && key.ends_with(GLOBAL_API_SCRIPT_PATH_KEY) {
|
||||
let script_path = PathBuf::from(value);
|
||||
scripts.push(script_path);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(tauri_global_scripts) = tauri_global_scripts {
|
||||
scripts.insert(0, tauri_global_scripts);
|
||||
}
|
||||
|
||||
fs::write(
|
||||
out_dir.join(GLOBAL_API_SCRIPT_FILE_LIST_PATH),
|
||||
serde_json::to_string(&scripts).expect("failed to serialize global API script paths"),
|
||||
)
|
||||
.expect("failed to write global API script");
|
||||
}
|
||||
|
||||
/// Read global api scripts from [`GLOBAL_API_SCRIPT_FILE_LIST_PATH`]
|
||||
pub fn read_global_api_scripts(out_dir: &Path) -> Option<Vec<String>> {
|
||||
let global_scripts_path = out_dir.join(GLOBAL_API_SCRIPT_FILE_LIST_PATH);
|
||||
if !global_scripts_path.exists() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let global_scripts_str = fs::read_to_string(global_scripts_path)
|
||||
.expect("failed to read plugin global API script paths");
|
||||
let global_scripts = serde_json::from_str::<Vec<PathBuf>>(&global_scripts_str)
|
||||
.expect("failed to parse plugin global API script paths");
|
||||
|
||||
Some(
|
||||
global_scripts
|
||||
.into_iter()
|
||||
.map(|p| {
|
||||
fs::read_to_string(&p).unwrap_or_else(|e| {
|
||||
panic!(
|
||||
"failed to read plugin global API script {}: {e}",
|
||||
p.display()
|
||||
)
|
||||
})
|
||||
})
|
||||
.collect(),
|
||||
)
|
||||
}
|
||||
}
|
||||
643
apps/tauri/src-tauri/vendor/tauri-utils/src/resources.rs
vendored
Normal file
643
apps/tauri/src-tauri/vendor/tauri-utils/src/resources.rs
vendored
Normal file
|
|
@ -0,0 +1,643 @@
|
|||
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
path::{Component, Path, PathBuf},
|
||||
};
|
||||
|
||||
use walkdir::WalkDir;
|
||||
|
||||
use crate::platform::Target as TargetPlatform;
|
||||
|
||||
/// Given a path (absolute or relative) to a resource file, returns the
|
||||
/// relative path from the bundle resources directory where that resource
|
||||
/// should be stored.
|
||||
pub fn resource_relpath(path: &Path) -> PathBuf {
|
||||
let mut dest = PathBuf::new();
|
||||
for component in path.components() {
|
||||
match component {
|
||||
Component::Prefix(_) => {}
|
||||
Component::RootDir => dest.push("_root_"),
|
||||
Component::CurDir => {}
|
||||
Component::ParentDir => dest.push("_up_"),
|
||||
Component::Normal(string) => dest.push(string),
|
||||
}
|
||||
}
|
||||
dest
|
||||
}
|
||||
|
||||
fn normalize(path: &Path) -> PathBuf {
|
||||
let mut dest = PathBuf::new();
|
||||
for component in path.components() {
|
||||
match component {
|
||||
Component::Prefix(_) => {}
|
||||
Component::RootDir => dest.push("/"),
|
||||
Component::CurDir => {}
|
||||
Component::ParentDir => dest.push(".."),
|
||||
Component::Normal(string) => dest.push(string),
|
||||
}
|
||||
}
|
||||
dest
|
||||
}
|
||||
|
||||
/// Parses the external binaries to bundle, adding the target triple suffix to each of them.
|
||||
pub fn external_binaries(
|
||||
external_binaries: &[String],
|
||||
target_triple: &str,
|
||||
target_platform: &TargetPlatform,
|
||||
) -> Vec<String> {
|
||||
let mut paths = Vec::new();
|
||||
for curr_path in external_binaries {
|
||||
let extension = if matches!(target_platform, TargetPlatform::Windows) {
|
||||
".exe"
|
||||
} else {
|
||||
""
|
||||
};
|
||||
paths.push(format!("{curr_path}-{target_triple}{extension}"));
|
||||
}
|
||||
paths
|
||||
}
|
||||
|
||||
/// Information for a resource.
|
||||
#[derive(Debug)]
|
||||
pub struct Resource {
|
||||
path: PathBuf,
|
||||
target: PathBuf,
|
||||
}
|
||||
|
||||
impl Resource {
|
||||
/// The path of the resource.
|
||||
pub fn path(&self) -> &Path {
|
||||
&self.path
|
||||
}
|
||||
|
||||
/// The target location of the resource.
|
||||
pub fn target(&self) -> &Path {
|
||||
&self.target
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
enum PatternIter<'a> {
|
||||
Slice(std::slice::Iter<'a, String>),
|
||||
Map(std::collections::hash_map::Iter<'a, String, String>),
|
||||
}
|
||||
|
||||
/// A helper to iterate through resources.
|
||||
pub struct ResourcePaths<'a> {
|
||||
iter: ResourcePathsIter<'a>,
|
||||
}
|
||||
|
||||
impl<'a> ResourcePaths<'a> {
|
||||
/// Creates a new ResourcePaths from a slice of patterns to iterate
|
||||
pub fn new(patterns: &'a [String], allow_walk: bool) -> ResourcePaths<'a> {
|
||||
ResourcePaths {
|
||||
iter: ResourcePathsIter {
|
||||
pattern_iter: PatternIter::Slice(patterns.iter()),
|
||||
allow_walk,
|
||||
current_dest: None,
|
||||
current_iter: None,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a new ResourcePaths from a slice of patterns to iterate
|
||||
pub fn from_map(patterns: &'a HashMap<String, String>, allow_walk: bool) -> ResourcePaths<'a> {
|
||||
ResourcePaths {
|
||||
iter: ResourcePathsIter {
|
||||
pattern_iter: PatternIter::Map(patterns.iter()),
|
||||
allow_walk,
|
||||
current_dest: None,
|
||||
current_iter: None,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the resource iterator that yields the source and target paths.
|
||||
/// Needed when using [`Self::from_map`].
|
||||
pub fn iter(self) -> ResourcePathsIter<'a> {
|
||||
self.iter
|
||||
}
|
||||
}
|
||||
|
||||
/// Iterator of a [`ResourcePaths`].
|
||||
#[derive(Debug)]
|
||||
pub struct ResourcePathsIter<'a> {
|
||||
/// the patterns to iterate.
|
||||
pattern_iter: PatternIter<'a>,
|
||||
/// whether the resource paths allows directories or not.
|
||||
allow_walk: bool,
|
||||
|
||||
/// The value of map when [`Self::pattern_iter`] is a [`PatternIter::Map`],
|
||||
/// used for determining [`Resource::target`]
|
||||
current_dest: Option<PathBuf>,
|
||||
/// The iter for the current pattern. The cycle goes like this:
|
||||
/// [`ResourcePaths::next`] -> [`Self::next`] -> [`Self::pattern_iter::next`] -> [`Self::current_iter::next`]
|
||||
current_iter: Option<ResourcePathsInnerIter>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
enum ResourcePathsInnerIter {
|
||||
Walk {
|
||||
iter: walkdir::IntoIter,
|
||||
/// The key of map when [`ResourcePathsIter::pattern_iter`] is a [`PatternIter::Map`],
|
||||
/// used for determining [`Resource::target`]
|
||||
current_pattern: Option<PathBuf>,
|
||||
},
|
||||
Glob {
|
||||
iter: glob::Paths,
|
||||
},
|
||||
}
|
||||
|
||||
impl Iterator for ResourcePathsInnerIter {
|
||||
type Item = crate::Result<PathBuf>;
|
||||
|
||||
fn next(&mut self) -> Option<crate::Result<PathBuf>> {
|
||||
match self {
|
||||
ResourcePathsInnerIter::Walk { iter, .. } => Some(
|
||||
iter
|
||||
.next()?
|
||||
.map(|entry| entry.into_path())
|
||||
.map_err(Into::into),
|
||||
),
|
||||
ResourcePathsInnerIter::Glob { iter } => Some(iter.next()?.map_err(Into::into)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ResourcePathsIter<'_> {
|
||||
fn next_current_iter(&mut self) -> Option<crate::Result<Resource>> {
|
||||
let current_iter = self.current_iter.as_mut().unwrap();
|
||||
let entry = current_iter.next()?;
|
||||
|
||||
Some(match entry {
|
||||
Ok(entry) => {
|
||||
// Skip directories
|
||||
if entry.is_dir() {
|
||||
self.next_current_iter()?
|
||||
} else {
|
||||
self.resource_from_path(normalize(&entry))
|
||||
}
|
||||
}
|
||||
Err(error) => Err(error),
|
||||
})
|
||||
}
|
||||
|
||||
fn resource_from_path(&self, path: PathBuf) -> crate::Result<Resource> {
|
||||
if !path.exists() {
|
||||
return Err(crate::Error::ResourcePathNotFound(path));
|
||||
}
|
||||
|
||||
Ok(Resource {
|
||||
target: if let Some(dest) = &self.current_dest {
|
||||
match &self.current_iter {
|
||||
Some(current_iter) => match current_iter {
|
||||
// if processing a directory, preserve directory structure under current_dest
|
||||
ResourcePathsInnerIter::Walk {
|
||||
current_pattern, ..
|
||||
} => {
|
||||
if let Some(pattern) = current_pattern {
|
||||
dest.join(path.strip_prefix(pattern).unwrap_or(&path))
|
||||
} else {
|
||||
dest.join(&path)
|
||||
}
|
||||
}
|
||||
// if processing a glob and current_dest is not empty
|
||||
// we put all globbed paths under current_dest
|
||||
// preserving the file name as it is
|
||||
ResourcePathsInnerIter::Glob { .. } => dest.join(path.file_name().unwrap()),
|
||||
},
|
||||
None => dest.clone(),
|
||||
}
|
||||
} else {
|
||||
// If [`ResourcePathsIter::pattern_iter`] is a [`PatternIter::Slice`]
|
||||
resource_relpath(&path)
|
||||
},
|
||||
path,
|
||||
})
|
||||
}
|
||||
|
||||
fn next_pattern(&mut self) -> Option<crate::Result<Resource>> {
|
||||
self.current_dest = None;
|
||||
|
||||
let pattern = match &mut self.pattern_iter {
|
||||
PatternIter::Slice(iter) => iter.next()?,
|
||||
PatternIter::Map(iter) => {
|
||||
let (pattern, dest) = iter.next()?;
|
||||
self.current_dest = Some(resource_relpath(Path::new(dest)));
|
||||
pattern
|
||||
}
|
||||
};
|
||||
|
||||
if pattern.contains('*') {
|
||||
self.current_iter = match glob::glob(pattern) {
|
||||
Ok(glob) => Some(ResourcePathsInnerIter::Glob { iter: glob }),
|
||||
Err(error) => return Some(Err(error.into())),
|
||||
};
|
||||
match self.next_current_iter() {
|
||||
Some(r) => return Some(r),
|
||||
None => {
|
||||
self.current_iter = None;
|
||||
return Some(Err(crate::Error::GlobPathNotFound(pattern.clone())));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let path = normalize(Path::new(pattern));
|
||||
if path.is_dir() {
|
||||
if !self.allow_walk {
|
||||
return Some(Err(crate::Error::NotAllowedToWalkDir(path)));
|
||||
}
|
||||
self.current_iter = Some(ResourcePathsInnerIter::Walk {
|
||||
iter: WalkDir::new(&path).into_iter(),
|
||||
current_pattern: if matches!(self.pattern_iter, PatternIter::Map(_)) {
|
||||
Some(path)
|
||||
} else {
|
||||
None
|
||||
},
|
||||
});
|
||||
} else {
|
||||
return Some(self.resource_from_path(path));
|
||||
}
|
||||
}
|
||||
|
||||
self.next_current_iter()
|
||||
}
|
||||
}
|
||||
|
||||
impl Iterator for ResourcePaths<'_> {
|
||||
type Item = crate::Result<PathBuf>;
|
||||
|
||||
fn next(&mut self) -> Option<crate::Result<PathBuf>> {
|
||||
self.iter.next().map(|r| r.map(|res| res.path))
|
||||
}
|
||||
}
|
||||
|
||||
impl Iterator for ResourcePathsIter<'_> {
|
||||
type Item = crate::Result<Resource>;
|
||||
|
||||
fn next(&mut self) -> Option<crate::Result<Resource>> {
|
||||
if self.current_iter.is_some() {
|
||||
match self.next_current_iter() {
|
||||
Some(r) => return Some(r),
|
||||
None => self.current_iter = None,
|
||||
}
|
||||
}
|
||||
|
||||
self.next_pattern()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
|
||||
use super::*;
|
||||
use std::fs;
|
||||
use std::path::Path;
|
||||
|
||||
impl PartialEq for Resource {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.path == other.path && self.target == other.target
|
||||
}
|
||||
}
|
||||
|
||||
fn expected_resources(resources: &[(&str, &str)]) -> Vec<Resource> {
|
||||
resources
|
||||
.iter()
|
||||
.map(|(path, target)| Resource {
|
||||
path: Path::new(path).components().collect(),
|
||||
target: Path::new(target).components().collect(),
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn setup_test_dirs() {
|
||||
let mut random = [0; 1];
|
||||
getrandom::fill(&mut random).unwrap();
|
||||
|
||||
let temp = std::env::temp_dir();
|
||||
let temp = temp.join(format!("tauri_resource_paths_iter_test_{}", random[0]));
|
||||
|
||||
let _ = fs::remove_dir_all(&temp);
|
||||
fs::create_dir_all(&temp).unwrap();
|
||||
|
||||
std::env::set_current_dir(&temp).unwrap();
|
||||
|
||||
let paths = [
|
||||
"src-tauri/tauri.conf.json",
|
||||
"src-tauri/some-other-json.json",
|
||||
"src-tauri/Cargo.toml",
|
||||
"src-tauri/Tauri.toml",
|
||||
"src-tauri/build.rs",
|
||||
"src-tauri/some-folder/some-file.txt",
|
||||
"src/assets/javascript.svg",
|
||||
"src/assets/tauri.svg",
|
||||
"src/assets/rust.svg",
|
||||
"src/assets/lang/en.json",
|
||||
"src/assets/lang/ar.json",
|
||||
"src/sounds/lang/es.wav",
|
||||
"src/sounds/lang/fr.wav",
|
||||
"src/textures/ground/earth.tex",
|
||||
"src/textures/ground/sand.tex",
|
||||
"src/textures/water.tex",
|
||||
"src/textures/fire.tex",
|
||||
"src/tiles/sky/grey.tile",
|
||||
"src/tiles/sky/yellow.tile",
|
||||
"src/tiles/grass.tile",
|
||||
"src/tiles/stones.tile",
|
||||
"src/index.html",
|
||||
"src/style.css",
|
||||
"src/script.js",
|
||||
"src/dir/another-dir/file1.txt",
|
||||
"src/dir/another-dir2/file2.txt",
|
||||
];
|
||||
|
||||
for path in paths {
|
||||
let path = Path::new(path);
|
||||
fs::create_dir_all(path.parent().unwrap()).unwrap();
|
||||
fs::write(path, "").unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
fn resources_map(literal: &[(&str, &str)]) -> HashMap<String, String> {
|
||||
literal
|
||||
.iter()
|
||||
.map(|(from, to)| (from.to_string(), to.to_string()))
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial_test::serial(resources)]
|
||||
fn resource_paths_iter_slice_allow_walk() {
|
||||
setup_test_dirs();
|
||||
|
||||
let dir = std::env::current_dir().unwrap().join("src-tauri");
|
||||
let _ = std::env::set_current_dir(dir);
|
||||
|
||||
let resources = ResourcePaths::new(
|
||||
&[
|
||||
"../src/script.js".into(),
|
||||
"../src/assets".into(),
|
||||
"../src/index.html".into(),
|
||||
"../src/sounds".into(),
|
||||
// Should be the same as `../src/textures/` or `../src/textures`
|
||||
"../src/textures/**/*".into(),
|
||||
"*.toml".into(),
|
||||
"*.conf.json".into(),
|
||||
],
|
||||
true,
|
||||
)
|
||||
.iter()
|
||||
.flatten()
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let expected = expected_resources(&[
|
||||
// From `../src/script.js`
|
||||
("../src/script.js", "_up_/src/script.js"),
|
||||
// From `../src/assets`
|
||||
(
|
||||
"../src/assets/javascript.svg",
|
||||
"_up_/src/assets/javascript.svg",
|
||||
),
|
||||
("../src/assets/tauri.svg", "_up_/src/assets/tauri.svg"),
|
||||
("../src/assets/rust.svg", "_up_/src/assets/rust.svg"),
|
||||
("../src/assets/lang/en.json", "_up_/src/assets/lang/en.json"),
|
||||
("../src/assets/lang/ar.json", "_up_/src/assets/lang/ar.json"),
|
||||
// From `../src/index.html`
|
||||
("../src/index.html", "_up_/src/index.html"),
|
||||
// From `../src/sounds`
|
||||
("../src/sounds/lang/es.wav", "_up_/src/sounds/lang/es.wav"),
|
||||
("../src/sounds/lang/fr.wav", "_up_/src/sounds/lang/fr.wav"),
|
||||
// From `../src/textures/**/*`
|
||||
(
|
||||
"../src/textures/ground/earth.tex",
|
||||
"_up_/src/textures/ground/earth.tex",
|
||||
),
|
||||
(
|
||||
"../src/textures/ground/sand.tex",
|
||||
"_up_/src/textures/ground/sand.tex",
|
||||
),
|
||||
("../src/textures/water.tex", "_up_/src/textures/water.tex"),
|
||||
("../src/textures/fire.tex", "_up_/src/textures/fire.tex"),
|
||||
// From `*.toml`
|
||||
("Cargo.toml", "Cargo.toml"),
|
||||
("Tauri.toml", "Tauri.toml"),
|
||||
// From `*.conf.json`
|
||||
("tauri.conf.json", "tauri.conf.json"),
|
||||
]);
|
||||
|
||||
assert_eq!(resources.len(), expected.len());
|
||||
for resource in expected {
|
||||
if !resources.contains(&resource) {
|
||||
panic!("{resource:?} was expected but not found in {resources:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial_test::serial(resources)]
|
||||
fn resource_paths_iter_slice_no_walk() {
|
||||
setup_test_dirs();
|
||||
|
||||
let dir = std::env::current_dir().unwrap().join("src-tauri");
|
||||
let _ = std::env::set_current_dir(dir);
|
||||
|
||||
let resources = ResourcePaths::new(
|
||||
&[
|
||||
"../src/script.js".into(),
|
||||
"../src/assets".into(),
|
||||
"../src/index.html".into(),
|
||||
"../src/sounds".into(),
|
||||
"*.toml".into(),
|
||||
"*.conf.json".into(),
|
||||
],
|
||||
false,
|
||||
)
|
||||
.iter()
|
||||
.flatten()
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let expected = expected_resources(&[
|
||||
("../src/script.js", "_up_/src/script.js"),
|
||||
("../src/index.html", "_up_/src/index.html"),
|
||||
("Cargo.toml", "Cargo.toml"),
|
||||
("Tauri.toml", "Tauri.toml"),
|
||||
("tauri.conf.json", "tauri.conf.json"),
|
||||
]);
|
||||
|
||||
assert_eq!(resources.len(), expected.len());
|
||||
for resource in expected {
|
||||
if !resources.contains(&resource) {
|
||||
panic!("{resource:?} was expected but not found in {resources:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial_test::serial(resources)]
|
||||
fn resource_paths_iter_map_allow_walk() {
|
||||
setup_test_dirs();
|
||||
|
||||
let dir = std::env::current_dir().unwrap().join("src-tauri");
|
||||
let _ = std::env::set_current_dir(dir);
|
||||
|
||||
let resources = ResourcePaths::from_map(
|
||||
&resources_map(&[
|
||||
("../src/script.js", "main.js"),
|
||||
("../src/assets", ""),
|
||||
("../src/index.html", "frontend/index.html"),
|
||||
("../src/sounds", "voices"),
|
||||
("../src/textures/*", "textures"),
|
||||
("../src/tiles/**/*", "tiles"),
|
||||
("*.toml", ""),
|
||||
("*.conf.json", "json"),
|
||||
("./some-folder/", "some-target-folder/"),
|
||||
("../non-existent-file", "asd"), // invalid case
|
||||
("../non/*", "asd"), // invalid case
|
||||
]),
|
||||
true,
|
||||
)
|
||||
.iter()
|
||||
.flatten()
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let expected = expected_resources(&[
|
||||
("../src/script.js", "main.js"),
|
||||
("../src/assets/javascript.svg", "javascript.svg"),
|
||||
("../src/assets/tauri.svg", "tauri.svg"),
|
||||
("../src/assets/rust.svg", "rust.svg"),
|
||||
("../src/assets/lang/en.json", "lang/en.json"),
|
||||
("../src/assets/lang/ar.json", "lang/ar.json"),
|
||||
("../src/index.html", "frontend/index.html"),
|
||||
("../src/sounds/lang/es.wav", "voices/lang/es.wav"),
|
||||
("../src/sounds/lang/fr.wav", "voices/lang/fr.wav"),
|
||||
("../src/textures/water.tex", "textures/water.tex"),
|
||||
("../src/textures/fire.tex", "textures/fire.tex"),
|
||||
("../src/tiles/grass.tile", "tiles/grass.tile"),
|
||||
("../src/tiles/stones.tile", "tiles/stones.tile"),
|
||||
("../src/tiles/sky/grey.tile", "tiles/grey.tile"),
|
||||
("../src/tiles/sky/yellow.tile", "tiles/yellow.tile"),
|
||||
("Cargo.toml", "Cargo.toml"),
|
||||
("Tauri.toml", "Tauri.toml"),
|
||||
("tauri.conf.json", "json/tauri.conf.json"),
|
||||
(
|
||||
"some-folder/some-file.txt",
|
||||
"some-target-folder/some-file.txt",
|
||||
),
|
||||
]);
|
||||
|
||||
assert_eq!(resources.len(), expected.len());
|
||||
for resource in expected {
|
||||
if !resources.contains(&resource) {
|
||||
panic!("{resource:?} was expected but not found in {resources:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial_test::serial(resources)]
|
||||
fn resource_paths_iter_map_no_walk() {
|
||||
setup_test_dirs();
|
||||
|
||||
let dir = std::env::current_dir().unwrap().join("src-tauri");
|
||||
let _ = std::env::set_current_dir(dir);
|
||||
|
||||
let resources = ResourcePaths::from_map(
|
||||
&resources_map(&[
|
||||
("../src/script.js", "main.js"),
|
||||
("../src/assets", ""),
|
||||
("../src/index.html", "frontend/index.html"),
|
||||
("../src/sounds", "voices"),
|
||||
("*.toml", ""),
|
||||
("*.conf.json", "json"),
|
||||
]),
|
||||
false,
|
||||
)
|
||||
.iter()
|
||||
.flatten()
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let expected = expected_resources(&[
|
||||
("../src/script.js", "main.js"),
|
||||
("../src/index.html", "frontend/index.html"),
|
||||
("Cargo.toml", "Cargo.toml"),
|
||||
("Tauri.toml", "Tauri.toml"),
|
||||
("tauri.conf.json", "json/tauri.conf.json"),
|
||||
]);
|
||||
|
||||
assert_eq!(resources.len(), expected.len());
|
||||
for resource in expected {
|
||||
if !resources.contains(&resource) {
|
||||
panic!("{resource:?} was expected but not found in {resources:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial_test::serial(resources)]
|
||||
fn resource_paths_errors() {
|
||||
setup_test_dirs();
|
||||
|
||||
let dir = std::env::current_dir().unwrap().join("src-tauri");
|
||||
let _ = std::env::set_current_dir(dir);
|
||||
|
||||
let resources = ResourcePaths::from_map(
|
||||
&resources_map(&[
|
||||
("../non-existent-file", "file"),
|
||||
("../non-existent-dir", "dir"),
|
||||
// exists but not allowed to walk
|
||||
("../src", "dir2"),
|
||||
// doesn't exist but it is a glob and will return an error
|
||||
("../non-existent-glob-dir/*", "glob"),
|
||||
// exists but only contains directories and will not produce any values
|
||||
("../src/dir/*", "dir3"),
|
||||
]),
|
||||
false,
|
||||
)
|
||||
.iter()
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
assert_eq!(resources.len(), 5);
|
||||
|
||||
assert!(resources.iter().all(|r| r.is_err()));
|
||||
|
||||
// hashmap order is not guaranteed so we check the error variant exists and how many
|
||||
assert!(
|
||||
resources
|
||||
.iter()
|
||||
.any(|r| matches!(r, Err(crate::Error::ResourcePathNotFound(_))))
|
||||
);
|
||||
assert_eq!(
|
||||
resources
|
||||
.iter()
|
||||
.filter(|r| matches!(r, Err(crate::Error::ResourcePathNotFound(_))))
|
||||
.count(),
|
||||
2
|
||||
);
|
||||
assert!(
|
||||
resources
|
||||
.iter()
|
||||
.any(|r| matches!(r, Err(crate::Error::NotAllowedToWalkDir(_))))
|
||||
);
|
||||
assert_eq!(
|
||||
resources
|
||||
.iter()
|
||||
.filter(|r| matches!(r, Err(crate::Error::NotAllowedToWalkDir(_))))
|
||||
.count(),
|
||||
1
|
||||
);
|
||||
assert!(
|
||||
resources
|
||||
.iter()
|
||||
.any(|r| matches!(r, Err(crate::Error::GlobPathNotFound(_))))
|
||||
);
|
||||
assert_eq!(
|
||||
resources
|
||||
.iter()
|
||||
.filter(|r| matches!(r, Err(crate::Error::GlobPathNotFound(_))))
|
||||
.count(),
|
||||
2
|
||||
);
|
||||
}
|
||||
}
|
||||
175
apps/tauri/src-tauri/vendor/tauri-utils/src/tokens.rs
vendored
Normal file
175
apps/tauri/src-tauri/vendor/tauri-utils/src/tokens.rs
vendored
Normal file
|
|
@ -0,0 +1,175 @@
|
|||
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
//! Utilities to implement [`ToTokens`] for a type.
|
||||
|
||||
use std::path::Path;
|
||||
|
||||
use proc_macro2::TokenStream;
|
||||
use quote::{ToTokens, quote};
|
||||
use serde_json::Value as JsonValue;
|
||||
use url::Url;
|
||||
|
||||
/// Write a `TokenStream` of the `$struct`'s fields to the `$tokens`.
|
||||
///
|
||||
/// All fields must represent a binding of the same name that implements `ToTokens`.
|
||||
#[macro_export]
|
||||
macro_rules! literal_struct {
|
||||
($tokens:ident, $struct:path, $($field:ident),+) => {
|
||||
$tokens.append_all(quote! {
|
||||
$struct {
|
||||
$($field: #$field),+
|
||||
}
|
||||
})
|
||||
};
|
||||
}
|
||||
|
||||
/// Create a `String` constructor `TokenStream`.
|
||||
///
|
||||
/// e.g. `"Hello World"` -> `String::from("Hello World")`.
|
||||
/// This takes a `&String` to reduce casting all the `&String` -> `&str` manually.
|
||||
pub fn str_lit(s: impl AsRef<str>) -> TokenStream {
|
||||
let s = s.as_ref();
|
||||
quote! { #s.into() }
|
||||
}
|
||||
|
||||
/// Create an `Option` constructor `TokenStream`.
|
||||
pub fn opt_lit(item: Option<&impl ToTokens>) -> TokenStream {
|
||||
match item {
|
||||
None => quote! { ::core::option::Option::None },
|
||||
Some(item) => quote! { ::core::option::Option::Some(#item) },
|
||||
}
|
||||
}
|
||||
|
||||
/// Create an `Option` constructor `TokenStream` over an owned [`ToTokens`] impl type.
|
||||
pub fn opt_lit_owned(item: Option<impl ToTokens>) -> TokenStream {
|
||||
match item {
|
||||
None => quote! { ::core::option::Option::None },
|
||||
Some(item) => quote! { ::core::option::Option::Some(#item) },
|
||||
}
|
||||
}
|
||||
|
||||
/// Helper function to combine an `opt_lit` with `str_lit`.
|
||||
pub fn opt_str_lit(item: Option<impl AsRef<str>>) -> TokenStream {
|
||||
opt_lit(item.map(str_lit).as_ref())
|
||||
}
|
||||
|
||||
/// Helper function to combine an `opt_lit` with a list of `str_lit`
|
||||
pub fn opt_vec_lit<Raw, Tokens>(
|
||||
item: Option<impl IntoIterator<Item = Raw>>,
|
||||
map: impl Fn(Raw) -> Tokens,
|
||||
) -> TokenStream
|
||||
where
|
||||
Tokens: ToTokens,
|
||||
{
|
||||
opt_lit(item.map(|list| vec_lit(list, map)).as_ref())
|
||||
}
|
||||
|
||||
/// Create a `Vec` constructor, mapping items with a function that spits out `TokenStream`s.
|
||||
pub fn vec_lit<Raw, Tokens>(
|
||||
list: impl IntoIterator<Item = Raw>,
|
||||
map: impl Fn(Raw) -> Tokens,
|
||||
) -> TokenStream
|
||||
where
|
||||
Tokens: ToTokens,
|
||||
{
|
||||
let items = list.into_iter().map(map);
|
||||
quote! { vec![#(#items),*] }
|
||||
}
|
||||
|
||||
/// Create a `PathBuf` constructor `TokenStream`.
|
||||
///
|
||||
/// e.g. `"Hello World" -> String::from("Hello World").
|
||||
pub fn path_buf_lit(s: impl AsRef<Path>) -> TokenStream {
|
||||
let s = s.as_ref().to_string_lossy().into_owned();
|
||||
quote! { ::std::path::PathBuf::from(#s) }
|
||||
}
|
||||
|
||||
/// Creates a `Url` constructor `TokenStream`.
|
||||
pub fn url_lit(url: &Url) -> TokenStream {
|
||||
let url = url.as_str();
|
||||
quote! { #url.parse().unwrap() }
|
||||
}
|
||||
|
||||
/// Create a map constructor, mapping keys and values with other `TokenStream`s.
|
||||
///
|
||||
/// This function is pretty generic because the types of keys AND values get transformed.
|
||||
pub fn map_lit<Map, Key, Value, TokenStreamKey, TokenStreamValue, FuncKey, FuncValue>(
|
||||
map_type: TokenStream,
|
||||
map: Map,
|
||||
map_key: FuncKey,
|
||||
map_value: FuncValue,
|
||||
) -> TokenStream
|
||||
where
|
||||
<Map as IntoIterator>::IntoIter: ExactSizeIterator,
|
||||
Map: IntoIterator<Item = (Key, Value)>,
|
||||
TokenStreamKey: ToTokens,
|
||||
TokenStreamValue: ToTokens,
|
||||
FuncKey: Fn(Key) -> TokenStreamKey,
|
||||
FuncValue: Fn(Value) -> TokenStreamValue,
|
||||
{
|
||||
let ident = quote::format_ident!("map");
|
||||
let map = map.into_iter();
|
||||
|
||||
if map.len() > 0 {
|
||||
let items = map.map(|(key, value)| {
|
||||
let key = map_key(key);
|
||||
let value = map_value(value);
|
||||
quote! { #ident.insert(#key, #value); }
|
||||
});
|
||||
|
||||
quote! {{
|
||||
let mut #ident = #map_type::new();
|
||||
#(#items)*
|
||||
#ident
|
||||
}}
|
||||
} else {
|
||||
quote! { #map_type::new() }
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a `serde_json::Value` variant `TokenStream` for a number
|
||||
pub fn json_value_number_lit(num: &serde_json::Number) -> TokenStream {
|
||||
// See <https://docs.rs/serde_json/1/serde_json/struct.Number.html> for guarantees
|
||||
let prefix = quote! { ::serde_json::Value };
|
||||
if num.is_u64() {
|
||||
// guaranteed u64
|
||||
let num = num.as_u64().unwrap();
|
||||
quote! { #prefix::Number(#num.into()) }
|
||||
} else if num.is_i64() {
|
||||
// guaranteed i64
|
||||
let num = num.as_i64().unwrap();
|
||||
quote! { #prefix::Number(#num.into()) }
|
||||
} else if num.is_f64() {
|
||||
// guaranteed f64
|
||||
let num = num.as_f64().unwrap();
|
||||
quote! { #prefix::Number(::serde_json::Number::from_f64(#num).unwrap(/* safe to unwrap, guaranteed f64 */)) }
|
||||
} else {
|
||||
// invalid number
|
||||
quote! { #prefix::Null }
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a `serde_json::Value` constructor `TokenStream`
|
||||
pub fn json_value_lit(jv: &JsonValue) -> TokenStream {
|
||||
let prefix = quote! { ::serde_json::Value };
|
||||
|
||||
match jv {
|
||||
JsonValue::Null => quote! { #prefix::Null },
|
||||
JsonValue::Bool(bool) => quote! { #prefix::Bool(#bool) },
|
||||
JsonValue::Number(number) => json_value_number_lit(number),
|
||||
JsonValue::String(str) => {
|
||||
let s = str_lit(str);
|
||||
quote! { #prefix::String(#s) }
|
||||
}
|
||||
JsonValue::Array(vec) => {
|
||||
let items = vec.iter().map(json_value_lit);
|
||||
quote! { #prefix::Array(vec![#(#items),*]) }
|
||||
}
|
||||
JsonValue::Object(map) => {
|
||||
let map = map_lit(quote! { ::serde_json::Map }, map, str_lit, json_value_lit);
|
||||
quote! { #prefix::Object(#map) }
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue