12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526 |
- // Copyright 2019-2023 Tauri Programme within The Commons Conservancy
- // SPDX-License-Identifier: Apache-2.0
- // SPDX-License-Identifier: MIT
- use std::{
- borrow::Cow,
- collections::{HashMap, HashSet},
- fmt,
- fs::create_dir_all,
- sync::{Arc, Mutex, MutexGuard},
- };
- use serde::Serialize;
- use serde_json::Value as JsonValue;
- use serialize_to_javascript::{default_template, DefaultTemplate, Template};
- use url::Url;
- use tauri_macros::default_runtime;
- use tauri_utils::debug_eprintln;
- #[cfg(feature = "isolation")]
- use tauri_utils::pattern::isolation::RawIsolationPayload;
- use tauri_utils::{
- assets::{AssetKey, CspHash},
- config::{Csp, CspDirectiveSources},
- html::{SCRIPT_NONCE_TOKEN, STYLE_NONCE_TOKEN},
- };
- use crate::hooks::IpcJavascript;
- #[cfg(feature = "isolation")]
- use crate::hooks::IsolationJavascript;
- use crate::pattern::{format_real_schema, PatternJavascript};
- use crate::{
- app::{AppHandle, GlobalWindowEvent, GlobalWindowEventListener},
- event::{assert_event_name_is_valid, Event, EventHandler, Listeners},
- hooks::{InvokeHandler, InvokePayload, InvokeResponder, OnPageLoad, PageLoadPayload},
- plugin::PluginStore,
- runtime::{
- http::{
- MimeType, Request as HttpRequest, Response as HttpResponse,
- ResponseBuilder as HttpResponseBuilder,
- },
- webview::{WebviewIpcHandler, WindowBuilder},
- window::{dpi::PhysicalSize, DetachedWindow, FileDropEvent, PendingWindow},
- },
- utils::{
- assets::Assets,
- config::{AppUrl, Config, WindowUrl},
- PackageInfo,
- },
- Context, EventLoopMessage, Icon, Invoke, Manager, Pattern, Runtime, Scopes, StateManager, Window,
- WindowEvent,
- };
- use crate::{
- app::{GlobalMenuEventListener, WindowMenuEvent},
- window::WebResourceRequestHandler,
- };
- #[cfg(any(target_os = "linux", target_os = "windows"))]
- use crate::api::path::{resolve_path, BaseDirectory};
- use crate::{runtime::menu::Menu, MenuEvent};
- const WINDOW_RESIZED_EVENT: &str = "tauri://resize";
- const WINDOW_MOVED_EVENT: &str = "tauri://move";
- const WINDOW_CLOSE_REQUESTED_EVENT: &str = "tauri://close-requested";
- const WINDOW_DESTROYED_EVENT: &str = "tauri://destroyed";
- const WINDOW_FOCUS_EVENT: &str = "tauri://focus";
- const WINDOW_BLUR_EVENT: &str = "tauri://blur";
- const WINDOW_SCALE_FACTOR_CHANGED_EVENT: &str = "tauri://scale-change";
- const WINDOW_THEME_CHANGED: &str = "tauri://theme-changed";
- const WINDOW_FILE_DROP_EVENT: &str = "tauri://file-drop";
- const WINDOW_FILE_DROP_HOVER_EVENT: &str = "tauri://file-drop-hover";
- const WINDOW_FILE_DROP_CANCELLED_EVENT: &str = "tauri://file-drop-cancelled";
- const MENU_EVENT: &str = "tauri://menu";
- pub(crate) const STRINGIFY_IPC_MESSAGE_FN: &str =
- include_str!("../scripts/stringify-ipc-message-fn.js");
- #[derive(Default)]
- /// Spaced and quoted Content-Security-Policy hash values.
- struct CspHashStrings {
- script: Vec<String>,
- style: Vec<String>,
- }
- /// Sets the CSP value to the asset HTML if needed (on Linux).
- /// Returns the CSP string for access on the response header (on Windows and macOS).
- fn set_csp<R: Runtime>(
- asset: &mut String,
- assets: Arc<dyn Assets>,
- asset_path: &AssetKey,
- manager: &WindowManager<R>,
- csp: Csp,
- ) -> String {
- let mut csp = csp.into();
- let hash_strings =
- assets
- .csp_hashes(asset_path)
- .fold(CspHashStrings::default(), |mut acc, hash| {
- match hash {
- CspHash::Script(hash) => {
- acc.script.push(hash.into());
- }
- CspHash::Style(hash) => {
- acc.style.push(hash.into());
- }
- _csp_hash => {
- debug_eprintln!("Unknown CspHash variant encountered: {:?}", _csp_hash);
- }
- }
- acc
- });
- let dangerous_disable_asset_csp_modification = &manager
- .config()
- .tauri
- .security
- .dangerous_disable_asset_csp_modification;
- if dangerous_disable_asset_csp_modification.can_modify("script-src") {
- replace_csp_nonce(
- asset,
- SCRIPT_NONCE_TOKEN,
- &mut csp,
- "script-src",
- hash_strings.script,
- );
- }
- if dangerous_disable_asset_csp_modification.can_modify("style-src") {
- replace_csp_nonce(
- asset,
- STYLE_NONCE_TOKEN,
- &mut csp,
- "style-src",
- hash_strings.style,
- );
- }
- #[cfg(feature = "isolation")]
- if let Pattern::Isolation { schema, .. } = &manager.inner.pattern {
- let default_src = csp
- .entry("default-src".into())
- .or_insert_with(Default::default);
- default_src.push(format_real_schema(schema));
- }
- Csp::DirectiveMap(csp).to_string()
- }
- #[cfg(target_os = "linux")]
- fn set_html_csp(html: &str, csp: &str) -> String {
- html.replacen(tauri_utils::html::CSP_TOKEN, csp, 1)
- }
- // inspired by https://github.com/rust-lang/rust/blob/1be5c8f90912c446ecbdc405cbc4a89f9acd20fd/library/alloc/src/str.rs#L260-L297
- fn replace_with_callback<F: FnMut() -> String>(
- original: &str,
- pattern: &str,
- mut replacement: F,
- ) -> String {
- let mut result = String::new();
- let mut last_end = 0;
- for (start, part) in original.match_indices(pattern) {
- result.push_str(unsafe { original.get_unchecked(last_end..start) });
- result.push_str(&replacement());
- last_end = start + part.len();
- }
- result.push_str(unsafe { original.get_unchecked(last_end..original.len()) });
- result
- }
- fn replace_csp_nonce(
- asset: &mut String,
- token: &str,
- csp: &mut HashMap<String, CspDirectiveSources>,
- directive: &str,
- hashes: Vec<String>,
- ) {
- let mut nonces = Vec::new();
- *asset = replace_with_callback(asset, token, || {
- let nonce = rand::random::<usize>();
- nonces.push(nonce);
- nonce.to_string()
- });
- if !(nonces.is_empty() && hashes.is_empty()) {
- let nonce_sources = nonces
- .into_iter()
- .map(|n| format!("'nonce-{n}'"))
- .collect::<Vec<String>>();
- let sources = csp.entry(directive.into()).or_insert_with(Default::default);
- let self_source = "'self'".to_string();
- if !sources.contains(&self_source) {
- sources.push(self_source);
- }
- sources.extend(nonce_sources);
- sources.extend(hashes);
- }
- }
- #[default_runtime(crate::Wry, wry)]
- pub struct InnerWindowManager<R: Runtime> {
- windows: Mutex<HashMap<String, Window<R>>>,
- #[cfg(all(desktop, feature = "system-tray"))]
- pub(crate) trays: Mutex<HashMap<String, crate::SystemTrayHandle<R>>>,
- pub(crate) plugins: Mutex<PluginStore<R>>,
- listeners: Listeners,
- pub(crate) state: Arc<StateManager>,
- /// The JS message handler.
- invoke_handler: Box<InvokeHandler<R>>,
- /// The page load hook, invoked when the webview performs a navigation.
- on_page_load: Box<OnPageLoad<R>>,
- config: Arc<Config>,
- assets: Arc<dyn Assets>,
- pub(crate) default_window_icon: Option<Icon>,
- pub(crate) app_icon: Option<Vec<u8>>,
- pub(crate) tray_icon: Option<Icon>,
- package_info: PackageInfo,
- /// The webview protocols available to all windows.
- uri_scheme_protocols: HashMap<String, Arc<CustomProtocol<R>>>,
- /// The menu set to all windows.
- menu: Option<Menu>,
- /// Menu event listeners to all windows.
- menu_event_listeners: Arc<Vec<GlobalMenuEventListener<R>>>,
- /// Window event listeners to all windows.
- window_event_listeners: Arc<Vec<GlobalWindowEventListener<R>>>,
- /// Responder for invoke calls.
- invoke_responder: Arc<InvokeResponder<R>>,
- /// The script that initializes the invoke system.
- invoke_initialization_script: String,
- /// Application pattern.
- pattern: Pattern,
- }
- impl<R: Runtime> fmt::Debug for InnerWindowManager<R> {
- fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
- f.debug_struct("InnerWindowManager")
- .field("plugins", &self.plugins)
- .field("state", &self.state)
- .field("config", &self.config)
- .field("default_window_icon", &self.default_window_icon)
- .field("app_icon", &self.app_icon)
- .field("tray_icon", &self.tray_icon)
- .field("package_info", &self.package_info)
- .field("menu", &self.menu)
- .field("pattern", &self.pattern)
- .finish()
- }
- }
- /// A resolved asset.
- pub struct Asset {
- /// The asset bytes.
- pub bytes: Vec<u8>,
- /// The asset's mime type.
- pub mime_type: String,
- /// The `Content-Security-Policy` header value.
- pub csp_header: Option<String>,
- }
- /// Uses a custom URI scheme handler to resolve file requests
- pub struct CustomProtocol<R: Runtime> {
- /// Handler for protocol
- #[allow(clippy::type_complexity)]
- pub protocol: Box<
- dyn Fn(&AppHandle<R>, &HttpRequest) -> Result<HttpResponse, Box<dyn std::error::Error>>
- + Send
- + Sync,
- >,
- }
- #[default_runtime(crate::Wry, wry)]
- #[derive(Debug)]
- pub struct WindowManager<R: Runtime> {
- pub inner: Arc<InnerWindowManager<R>>,
- }
- impl<R: Runtime> Clone for WindowManager<R> {
- fn clone(&self) -> Self {
- Self {
- inner: self.inner.clone(),
- }
- }
- }
- impl<R: Runtime> WindowManager<R> {
- #[allow(clippy::too_many_arguments)]
- pub(crate) fn with_handlers(
- #[allow(unused_mut)] mut context: Context<impl Assets>,
- plugins: PluginStore<R>,
- invoke_handler: Box<InvokeHandler<R>>,
- on_page_load: Box<OnPageLoad<R>>,
- uri_scheme_protocols: HashMap<String, Arc<CustomProtocol<R>>>,
- state: StateManager,
- window_event_listeners: Vec<GlobalWindowEventListener<R>>,
- (menu, menu_event_listeners): (Option<Menu>, Vec<GlobalMenuEventListener<R>>),
- (invoke_responder, invoke_initialization_script): (Arc<InvokeResponder<R>>, String),
- ) -> Self {
- // generate a random isolation key at runtime
- #[cfg(feature = "isolation")]
- if let Pattern::Isolation { ref mut key, .. } = &mut context.pattern {
- *key = uuid::Uuid::new_v4().to_string();
- }
- Self {
- inner: Arc::new(InnerWindowManager {
- windows: Mutex::default(),
- #[cfg(all(desktop, feature = "system-tray"))]
- trays: Default::default(),
- plugins: Mutex::new(plugins),
- listeners: Listeners::default(),
- state: Arc::new(state),
- invoke_handler,
- on_page_load,
- config: Arc::new(context.config),
- assets: context.assets,
- default_window_icon: context.default_window_icon,
- app_icon: context.app_icon,
- tray_icon: context.system_tray_icon,
- package_info: context.package_info,
- pattern: context.pattern,
- uri_scheme_protocols,
- menu,
- menu_event_listeners: Arc::new(menu_event_listeners),
- window_event_listeners: Arc::new(window_event_listeners),
- invoke_responder,
- invoke_initialization_script,
- }),
- }
- }
- pub(crate) fn pattern(&self) -> &Pattern {
- &self.inner.pattern
- }
- /// Get a locked handle to the windows.
- pub(crate) fn windows_lock(&self) -> MutexGuard<'_, HashMap<String, Window<R>>> {
- self.inner.windows.lock().expect("poisoned window manager")
- }
- /// State managed by the application.
- pub(crate) fn state(&self) -> Arc<StateManager> {
- self.inner.state.clone()
- }
- /// The invoke responder.
- pub(crate) fn invoke_responder(&self) -> Arc<InvokeResponder<R>> {
- self.inner.invoke_responder.clone()
- }
- /// Get the base path to serve data from.
- ///
- /// * In dev mode, this will be based on the `devPath` configuration value.
- /// * Otherwise, this will be based on the `distDir` configuration value.
- #[cfg(not(dev))]
- fn base_path(&self) -> &AppUrl {
- &self.inner.config.build.dist_dir
- }
- #[cfg(dev)]
- fn base_path(&self) -> &AppUrl {
- &self.inner.config.build.dev_path
- }
- /// Get the base URL to use for webview requests.
- ///
- /// In dev mode, this will be based on the `devPath` configuration value.
- fn get_url(&self) -> Cow<'_, Url> {
- match self.base_path() {
- AppUrl::Url(WindowUrl::External(url)) => Cow::Borrowed(url),
- _ => Cow::Owned(Url::parse("tauri://localhost").unwrap()),
- }
- }
- /// Get the origin as it will be seen in the webview.
- fn get_browser_origin(&self) -> String {
- match self.base_path() {
- AppUrl::Url(WindowUrl::External(url)) => url.origin().ascii_serialization(),
- _ => format_real_schema("tauri"),
- }
- }
- fn csp(&self) -> Option<Csp> {
- if cfg!(feature = "custom-protocol") {
- self.inner.config.tauri.security.csp.clone()
- } else {
- self
- .inner
- .config
- .tauri
- .security
- .dev_csp
- .clone()
- .or_else(|| self.inner.config.tauri.security.csp.clone())
- }
- }
- fn prepare_pending_window(
- &self,
- mut pending: PendingWindow<EventLoopMessage, R>,
- label: &str,
- window_labels: &[String],
- app_handle: AppHandle<R>,
- web_resource_request_handler: Option<Box<WebResourceRequestHandler>>,
- ) -> crate::Result<PendingWindow<EventLoopMessage, R>> {
- let is_init_global = self.inner.config.build.with_global_tauri;
- let plugin_init = self
- .inner
- .plugins
- .lock()
- .expect("poisoned plugin store")
- .initialization_script();
- let pattern_init = PatternJavascript {
- pattern: self.pattern().into(),
- }
- .render_default(&Default::default())?;
- let ipc_init = IpcJavascript {
- isolation_origin: &match self.pattern() {
- #[cfg(feature = "isolation")]
- Pattern::Isolation { schema, .. } => crate::pattern::format_real_schema(schema),
- _ => "".to_string(),
- },
- }
- .render_default(&Default::default())?;
- let mut webview_attributes = pending.webview_attributes;
- let mut window_labels = window_labels.to_vec();
- let l = label.to_string();
- if !window_labels.contains(&l) {
- window_labels.push(l);
- }
- webview_attributes = webview_attributes
- .initialization_script(&self.inner.invoke_initialization_script)
- .initialization_script(&format!(
- r#"
- Object.defineProperty(window, '__TAURI_METADATA__', {{
- value: {{
- __windows: {window_labels_array}.map(function (label) {{ return {{ label: label }} }}),
- __currentWindow: {{ label: {current_window_label} }}
- }}
- }})
- "#,
- window_labels_array = serde_json::to_string(&window_labels)?,
- current_window_label = serde_json::to_string(&label)?,
- ))
- .initialization_script(&self.initialization_script(&ipc_init.into_string(),&pattern_init.into_string(),&plugin_init, is_init_global)?)
- ;
- #[cfg(feature = "isolation")]
- if let Pattern::Isolation { schema, .. } = self.pattern() {
- webview_attributes = webview_attributes.initialization_script(
- &IsolationJavascript {
- origin: self.get_browser_origin(),
- isolation_src: &crate::pattern::format_real_schema(schema),
- style: tauri_utils::pattern::isolation::IFRAME_STYLE,
- }
- .render_default(&Default::default())?
- .into_string(),
- );
- }
- pending.webview_attributes = webview_attributes;
- let mut registered_scheme_protocols = Vec::new();
- for (uri_scheme, protocol) in &self.inner.uri_scheme_protocols {
- registered_scheme_protocols.push(uri_scheme.clone());
- let protocol = protocol.clone();
- let app_handle = Mutex::new(app_handle.clone());
- pending.register_uri_scheme_protocol(uri_scheme.clone(), move |p| {
- (protocol.protocol)(&app_handle.lock().unwrap(), p)
- });
- }
- let window_url = Url::parse(&pending.url).unwrap();
- let window_origin =
- if cfg!(windows) && window_url.scheme() != "http" && window_url.scheme() != "https" {
- format!("https://{}.localhost", window_url.scheme())
- } else {
- format!(
- "{}://{}{}",
- window_url.scheme(),
- window_url.host().unwrap(),
- if let Some(port) = window_url.port() {
- format!(":{port}")
- } else {
- "".into()
- }
- )
- };
- if !registered_scheme_protocols.contains(&"tauri".into()) {
- pending.register_uri_scheme_protocol(
- "tauri",
- self.prepare_uri_scheme_protocol(&window_origin, web_resource_request_handler),
- );
- registered_scheme_protocols.push("tauri".into());
- }
- #[cfg(protocol_asset)]
- if !registered_scheme_protocols.contains(&"asset".into()) {
- use crate::api::file::SafePathBuf;
- use tokio::io::{AsyncReadExt, AsyncSeekExt};
- use url::Position;
- let asset_scope = self.state().get::<crate::Scopes>().asset_protocol.clone();
- pending.register_uri_scheme_protocol("asset", move |request| {
- let parsed_path = Url::parse(request.uri())?;
- let filtered_path = &parsed_path[..Position::AfterPath];
- let path = filtered_path
- .strip_prefix("asset://localhost/")
- // the `strip_prefix` only returns None when a request is made to `https://tauri.$P` on Windows
- // where `$P` is not `localhost/*`
- .unwrap_or("");
- let path = percent_encoding::percent_decode(path.as_bytes())
- .decode_utf8_lossy()
- .to_string();
- if let Err(e) = SafePathBuf::new(path.clone().into()) {
- debug_eprintln!("asset protocol path \"{}\" is not valid: {}", path, e);
- return HttpResponseBuilder::new().status(403).body(Vec::new());
- }
- if !asset_scope.is_allowed(&path) {
- debug_eprintln!("asset protocol not configured to allow the path: {}", path);
- return HttpResponseBuilder::new().status(403).body(Vec::new());
- }
- let path_ = path.clone();
- let mut response =
- HttpResponseBuilder::new().header("Access-Control-Allow-Origin", &window_origin);
- // handle 206 (partial range) http request
- if let Some(range) = request
- .headers()
- .get("range")
- .and_then(|r| r.to_str().map(|r| r.to_string()).ok())
- {
- #[derive(Default)]
- struct RangeMetadata {
- file: Option<tokio::fs::File>,
- range: Option<crate::runtime::http::HttpRange>,
- metadata: Option<std::fs::Metadata>,
- headers: HashMap<&'static str, String>,
- status_code: u16,
- body: Vec<u8>,
- }
- let mut range_metadata = crate::async_runtime::safe_block_on(async move {
- let mut data = RangeMetadata::default();
- // open the file
- let mut file = match tokio::fs::File::open(path_.clone()).await {
- Ok(file) => file,
- Err(e) => {
- debug_eprintln!("Failed to open asset: {}", e);
- data.status_code = 404;
- return data;
- }
- };
- // Get the file size
- let file_size = match file.metadata().await {
- Ok(metadata) => {
- let len = metadata.len();
- data.metadata.replace(metadata);
- len
- }
- Err(e) => {
- debug_eprintln!("Failed to read asset metadata: {}", e);
- data.file.replace(file);
- data.status_code = 404;
- return data;
- }
- };
- // parse the range
- let range = match crate::runtime::http::HttpRange::parse(
- &if range.ends_with("-*") {
- range.chars().take(range.len() - 1).collect::<String>()
- } else {
- range.clone()
- },
- file_size,
- ) {
- Ok(r) => r,
- Err(e) => {
- debug_eprintln!("Failed to parse range {}: {:?}", range, e);
- data.file.replace(file);
- data.status_code = 400;
- return data;
- }
- };
- // FIXME: Support multiple ranges
- // let support only 1 range for now
- if let Some(range) = range.first() {
- data.range.replace(*range);
- let mut real_length = range.length;
- // prevent max_length;
- // specially on webview2
- if range.length > file_size / 3 {
- // max size sent (400ko / request)
- // as it's local file system we can afford to read more often
- real_length = std::cmp::min(file_size - range.start, 1024 * 400);
- }
- // last byte we are reading, the length of the range include the last byte
- // who should be skipped on the header
- let last_byte = range.start + real_length - 1;
- data.headers.insert("Connection", "Keep-Alive".into());
- data.headers.insert("Accept-Ranges", "bytes".into());
- data
- .headers
- .insert("Content-Length", real_length.to_string());
- data.headers.insert(
- "Content-Range",
- format!("bytes {}-{last_byte}/{file_size}", range.start),
- );
- if let Err(e) = file.seek(std::io::SeekFrom::Start(range.start)).await {
- debug_eprintln!("Failed to seek file to {}: {}", range.start, e);
- data.file.replace(file);
- data.status_code = 422;
- return data;
- }
- let mut f = file.take(real_length);
- let r = f.read_to_end(&mut data.body).await;
- file = f.into_inner();
- data.file.replace(file);
- if let Err(e) = r {
- debug_eprintln!("Failed read file: {}", e);
- data.status_code = 422;
- return data;
- }
- // partial content
- data.status_code = 206;
- } else {
- data.status_code = 200;
- }
- data
- });
- for (k, v) in range_metadata.headers {
- response = response.header(k, v);
- }
- let mime_type = if let (Some(mut file), Some(metadata), Some(range)) = (
- range_metadata.file,
- range_metadata.metadata,
- range_metadata.range,
- ) {
- // if we're already reading the beginning of the file, we do not need to re-read it
- if range.start == 0 {
- MimeType::parse(&range_metadata.body, &path)
- } else {
- let (status, bytes) = crate::async_runtime::safe_block_on(async move {
- let mut status = None;
- if let Err(e) = file.rewind().await {
- debug_eprintln!("Failed to rewind file: {}", e);
- status.replace(422);
- (status, Vec::with_capacity(0))
- } else {
- // taken from https://docs.rs/infer/0.9.0/src/infer/lib.rs.html#240-251
- let limit = std::cmp::min(metadata.len(), 8192) as usize + 1;
- let mut bytes = Vec::with_capacity(limit);
- if let Err(e) = file.take(8192).read_to_end(&mut bytes).await {
- debug_eprintln!("Failed read file: {}", e);
- status.replace(422);
- }
- (status, bytes)
- }
- });
- if let Some(s) = status {
- range_metadata.status_code = s;
- }
- MimeType::parse(&bytes, &path)
- }
- } else {
- MimeType::parse(&range_metadata.body, &path)
- };
- response
- .mimetype(&mime_type)
- .status(range_metadata.status_code)
- .body(range_metadata.body)
- } else {
- match crate::async_runtime::safe_block_on(async move { tokio::fs::read(path_).await }) {
- Ok(data) => {
- let mime_type = MimeType::parse(&data, &path);
- response.mimetype(&mime_type).body(data)
- }
- Err(e) => {
- debug_eprintln!("Failed to read file: {}", e);
- response.status(404).body(Vec::new())
- }
- }
- }
- });
- }
- #[cfg(feature = "isolation")]
- if let Pattern::Isolation {
- assets,
- schema,
- key: _,
- crypto_keys,
- } = &self.inner.pattern
- {
- let assets = assets.clone();
- let schema_ = schema.clone();
- let url_base = format!("{schema_}://localhost");
- let aes_gcm_key = *crypto_keys.aes_gcm().raw();
- pending.register_uri_scheme_protocol(schema, move |request| {
- match request_to_path(request, &url_base).as_str() {
- "index.html" => match assets.get(&"index.html".into()) {
- Some(asset) => {
- let asset = String::from_utf8_lossy(asset.as_ref());
- let template = tauri_utils::pattern::isolation::IsolationJavascriptRuntime {
- runtime_aes_gcm_key: &aes_gcm_key,
- stringify_ipc_message_fn: STRINGIFY_IPC_MESSAGE_FN,
- };
- match template.render(asset.as_ref(), &Default::default()) {
- Ok(asset) => HttpResponseBuilder::new()
- .mimetype("text/html")
- .body(asset.into_string().as_bytes().to_vec()),
- Err(_) => HttpResponseBuilder::new()
- .status(500)
- .mimetype("text/plain")
- .body(Vec::new()),
- }
- }
- None => HttpResponseBuilder::new()
- .status(404)
- .mimetype("text/plain")
- .body(Vec::new()),
- },
- _ => HttpResponseBuilder::new()
- .status(404)
- .mimetype("text/plain")
- .body(Vec::new()),
- }
- });
- }
- Ok(pending)
- }
- fn prepare_ipc_handler(
- &self,
- app_handle: AppHandle<R>,
- ) -> WebviewIpcHandler<EventLoopMessage, R> {
- let manager = self.clone();
- Box::new(move |window, #[allow(unused_mut)] mut request| {
- let window = Window::new(manager.clone(), window, app_handle.clone());
- #[cfg(feature = "isolation")]
- if let Pattern::Isolation { crypto_keys, .. } = manager.pattern() {
- match RawIsolationPayload::try_from(request.as_str())
- .and_then(|raw| crypto_keys.decrypt(raw))
- {
- Ok(json) => request = json,
- Err(e) => {
- let error: crate::Error = e.into();
- let _ = window.eval(&format!(
- r#"console.error({})"#,
- JsonValue::String(error.to_string())
- ));
- return;
- }
- }
- }
- match serde_json::from_str::<InvokePayload>(&request) {
- Ok(message) => {
- let _ = window.on_message(message);
- }
- Err(e) => {
- let error: crate::Error = e.into();
- let _ = window.eval(&format!(
- r#"console.error({})"#,
- JsonValue::String(error.to_string())
- ));
- }
- }
- })
- }
- pub fn get_asset(&self, mut path: String) -> Result<Asset, Box<dyn std::error::Error>> {
- let assets = &self.inner.assets;
- if path.ends_with('/') {
- path.pop();
- }
- path = percent_encoding::percent_decode(path.as_bytes())
- .decode_utf8_lossy()
- .to_string();
- let path = if path.is_empty() {
- // if the url is `tauri://localhost`, we should load `index.html`
- "index.html".to_string()
- } else {
- // skip leading `/`
- path.chars().skip(1).collect::<String>()
- };
- let mut asset_path = AssetKey::from(path.as_str());
- let asset_response = assets
- .get(&path.as_str().into())
- .or_else(|| {
- eprintln!("Asset `{path}` not found; fallback to {path}.html");
- let fallback = format!("{}.html", path.as_str()).into();
- let asset = assets.get(&fallback);
- asset_path = fallback;
- asset
- })
- .or_else(|| {
- debug_eprintln!(
- "Asset `{}` not found; fallback to {}/index.html",
- path,
- path
- );
- let fallback = format!("{}/index.html", path.as_str()).into();
- let asset = assets.get(&fallback);
- asset_path = fallback;
- asset
- })
- .or_else(|| {
- debug_eprintln!("Asset `{}` not found; fallback to index.html", path);
- let fallback = AssetKey::from("index.html");
- let asset = assets.get(&fallback);
- asset_path = fallback;
- asset
- })
- .ok_or_else(|| crate::Error::AssetNotFound(path.clone()))
- .map(Cow::into_owned);
- let mut csp_header = None;
- let is_html = asset_path.as_ref().ends_with(".html");
- match asset_response {
- Ok(asset) => {
- let final_data = if is_html {
- let mut asset = String::from_utf8_lossy(&asset).into_owned();
- if let Some(csp) = self.csp() {
- csp_header.replace(set_csp(
- &mut asset,
- self.inner.assets.clone(),
- &asset_path,
- self,
- csp,
- ));
- }
- asset.as_bytes().to_vec()
- } else {
- asset
- };
- let mime_type = MimeType::parse(&final_data, &path);
- Ok(Asset {
- bytes: final_data.to_vec(),
- mime_type,
- csp_header,
- })
- }
- Err(e) => {
- debug_eprintln!("{:?}", e); // TODO log::error!
- Err(Box::new(e))
- }
- }
- }
- #[allow(clippy::type_complexity)]
- fn prepare_uri_scheme_protocol(
- &self,
- window_origin: &str,
- web_resource_request_handler: Option<
- Box<dyn Fn(&HttpRequest, &mut HttpResponse) + Send + Sync>,
- >,
- ) -> Box<dyn Fn(&HttpRequest) -> Result<HttpResponse, Box<dyn std::error::Error>> + Send + Sync>
- {
- let manager = self.clone();
- let window_origin = window_origin.to_string();
- Box::new(move |request| {
- let path = request
- .uri()
- .split(&['?', '#'][..])
- // ignore query string and fragment
- .next()
- .unwrap()
- .strip_prefix("tauri://localhost")
- .map(|p| p.to_string())
- // the `strip_prefix` only returns None when a request is made to `https://tauri.$P` on Windows
- // where `$P` is not `localhost/*`
- .unwrap_or_else(|| "".to_string());
- let asset = manager.get_asset(path)?;
- let mut builder = HttpResponseBuilder::new()
- .header("Access-Control-Allow-Origin", &window_origin)
- .mimetype(&asset.mime_type);
- if let Some(csp) = &asset.csp_header {
- builder = builder.header("Content-Security-Policy", csp);
- }
- let mut response = builder.body(asset.bytes)?;
- if let Some(handler) = &web_resource_request_handler {
- handler(request, &mut response);
- // if it's an HTML file, we need to set the CSP meta tag on Linux
- #[cfg(target_os = "linux")]
- if let Some(response_csp) = response.headers().get("Content-Security-Policy") {
- let response_csp = String::from_utf8_lossy(response_csp.as_bytes());
- let body = set_html_csp(&String::from_utf8_lossy(response.body()), &response_csp);
- *response.body_mut() = body.as_bytes().to_vec();
- }
- } else {
- #[cfg(target_os = "linux")]
- {
- if let Some(csp) = &asset.csp_header {
- let body = set_html_csp(&String::from_utf8_lossy(response.body()), csp);
- *response.body_mut() = body.as_bytes().to_vec();
- }
- }
- }
- Ok(response)
- })
- }
- fn initialization_script(
- &self,
- ipc_script: &str,
- pattern_script: &str,
- plugin_initialization_script: &str,
- with_global_tauri: bool,
- ) -> crate::Result<String> {
- #[derive(Template)]
- #[default_template("../scripts/init.js")]
- struct InitJavascript<'a> {
- origin: String,
- #[raw]
- pattern_script: &'a str,
- #[raw]
- ipc_script: &'a str,
- #[raw]
- bundle_script: &'a str,
- // A function to immediately listen to an event.
- #[raw]
- listen_function: &'a str,
- #[raw]
- core_script: &'a str,
- #[raw]
- event_initialization_script: &'a str,
- #[raw]
- plugin_initialization_script: &'a str,
- #[raw]
- freeze_prototype: &'a str,
- #[raw]
- hotkeys: &'a str,
- }
- let bundle_script = if with_global_tauri {
- include_str!("../scripts/bundle.global.js")
- } else {
- ""
- };
- let freeze_prototype = if self.inner.config.tauri.security.freeze_prototype {
- include_str!("../scripts/freeze_prototype.js")
- } else {
- ""
- };
- #[cfg(any(debug_assertions, feature = "devtools"))]
- let hotkeys = &format!(
- "
- {};
- window.hotkeys('{}', () => {{
- window.__TAURI_INVOKE__('tauri', {{
- __tauriModule: 'Window',
- message: {{
- cmd: 'manage',
- data: {{
- cmd: {{
- type: '__toggleDevtools'
- }}
- }}
- }}
- }});
- }});
- ",
- include_str!("../scripts/hotkey.js"),
- if cfg!(target_os = "macos") {
- "command+option+i"
- } else {
- "ctrl+shift+i"
- }
- );
- #[cfg(not(any(debug_assertions, feature = "devtools")))]
- let hotkeys = "";
- InitJavascript {
- origin: self.get_browser_origin(),
- pattern_script,
- ipc_script,
- bundle_script,
- listen_function: &format!(
- "function listen(eventName, cb) {{ {} }}",
- crate::event::listen_js(
- self.event_listeners_object_name(),
- "eventName".into(),
- 0,
- None,
- "window['_' + window.__TAURI__.transformCallback(cb) ]".into()
- )
- ),
- core_script: include_str!("../scripts/core.js"),
- event_initialization_script: &self.event_initialization_script(),
- plugin_initialization_script,
- freeze_prototype,
- hotkeys,
- }
- .render_default(&Default::default())
- .map(|s| s.into_string())
- .map_err(Into::into)
- }
- fn event_initialization_script(&self) -> String {
- format!(
- "
- Object.defineProperty(window, '{function}', {{
- value: function (eventData) {{
- const listeners = (window['{listeners}'] && window['{listeners}'][eventData.event]) || []
- for (let i = listeners.length - 1; i >= 0; i--) {{
- const listener = listeners[i]
- if (listener.windowLabel === null || listener.windowLabel === eventData.windowLabel) {{
- eventData.id = listener.id
- listener.handler(eventData)
- }}
- }}
- }}
- }});
- ",
- function = self.event_emit_function_name(),
- listeners = self.event_listeners_object_name()
- )
- }
- }
- #[cfg(test)]
- mod test {
- use crate::{generate_context, plugin::PluginStore, StateManager, Wry};
- use super::WindowManager;
- #[test]
- fn check_get_url() {
- let context = generate_context!("test/fixture/src-tauri/tauri.conf.json", crate);
- let manager: WindowManager<Wry> = WindowManager::with_handlers(
- context,
- PluginStore::default(),
- Box::new(|_| ()),
- Box::new(|_, _| ()),
- Default::default(),
- StateManager::new(),
- Default::default(),
- Default::default(),
- (std::sync::Arc::new(|_, _, _, _| ()), "".into()),
- );
- #[cfg(custom_protocol)]
- assert_eq!(manager.get_url().to_string(), "tauri://localhost");
- #[cfg(dev)]
- assert_eq!(manager.get_url().to_string(), "http://localhost:4000/");
- }
- }
- impl<R: Runtime> WindowManager<R> {
- pub fn run_invoke_handler(&self, invoke: Invoke<R>) {
- (self.inner.invoke_handler)(invoke);
- }
- pub fn run_on_page_load(&self, window: Window<R>, payload: PageLoadPayload) {
- (self.inner.on_page_load)(window.clone(), payload.clone());
- self
- .inner
- .plugins
- .lock()
- .expect("poisoned plugin store")
- .on_page_load(window, payload);
- }
- pub fn extend_api(&self, invoke: Invoke<R>) {
- self
- .inner
- .plugins
- .lock()
- .expect("poisoned plugin store")
- .extend_api(invoke);
- }
- pub fn initialize_plugins(&self, app: &AppHandle<R>) -> crate::Result<()> {
- self
- .inner
- .plugins
- .lock()
- .expect("poisoned plugin store")
- .initialize(app, &self.inner.config.plugins)
- }
- pub fn prepare_window(
- &self,
- app_handle: AppHandle<R>,
- mut pending: PendingWindow<EventLoopMessage, R>,
- window_labels: &[String],
- web_resource_request_handler: Option<Box<WebResourceRequestHandler>>,
- ) -> crate::Result<PendingWindow<EventLoopMessage, R>> {
- if self.windows_lock().contains_key(&pending.label) {
- return Err(crate::Error::WindowLabelAlreadyExists(pending.label));
- }
- #[allow(unused_mut)] // mut url only for the data-url parsing
- let (is_local, mut url) = match &pending.webview_attributes.url {
- WindowUrl::App(path) => {
- let url = self.get_url();
- (
- true,
- // ignore "index.html" just to simplify the url
- if path.to_str() != Some("index.html") {
- url
- .join(&path.to_string_lossy())
- .map_err(crate::Error::InvalidUrl)
- // this will never fail
- .unwrap()
- } else {
- url.into_owned()
- },
- )
- }
- WindowUrl::External(url) => {
- let config_url = self.get_url();
- (config_url.make_relative(url).is_some(), url.clone())
- }
- _ => unimplemented!(),
- };
- #[cfg(not(feature = "window-data-url"))]
- if url.scheme() == "data" {
- return Err(crate::Error::InvalidWindowUrl(
- "data URLs are not supported without the `window-data-url` feature.",
- ));
- }
- #[cfg(feature = "window-data-url")]
- if let Some(csp) = self.csp() {
- if url.scheme() == "data" {
- if let Ok(data_url) = data_url::DataUrl::process(url.as_str()) {
- let (body, _) = data_url.decode_to_vec().unwrap();
- let html = String::from_utf8_lossy(&body).into_owned();
- // naive way to check if it's an html
- if html.contains('<') && html.contains('>') {
- let mut document = tauri_utils::html::parse(html);
- tauri_utils::html::inject_csp(&mut document, &csp.to_string());
- url.set_path(&format!("text/html,{}", document.to_string()));
- }
- }
- }
- }
- pending.url = url.to_string();
- if !pending.window_builder.has_icon() {
- if let Some(default_window_icon) = self.inner.default_window_icon.clone() {
- pending.window_builder = pending
- .window_builder
- .icon(default_window_icon.try_into()?)?;
- }
- }
- if pending.window_builder.get_menu().is_none() {
- if let Some(menu) = &self.inner.menu {
- pending = pending.set_menu(menu.clone());
- }
- }
- if is_local {
- let label = pending.label.clone();
- pending = self.prepare_pending_window(
- pending,
- &label,
- window_labels,
- app_handle.clone(),
- web_resource_request_handler,
- )?;
- pending.ipc_handler = Some(self.prepare_ipc_handler(app_handle));
- }
- // in `Windows`, we need to force a data_directory
- // but we do respect user-specification
- #[cfg(any(target_os = "linux", target_os = "windows"))]
- if pending.webview_attributes.data_directory.is_none() {
- let local_app_data = resolve_path(
- &self.inner.config,
- &self.inner.package_info,
- self.inner.state.get::<crate::Env>().inner(),
- &self.inner.config.tauri.bundle.identifier,
- Some(BaseDirectory::LocalData),
- );
- if let Ok(user_data_dir) = local_app_data {
- pending.webview_attributes.data_directory = Some(user_data_dir);
- }
- }
- // make sure the directory is created and available to prevent a panic
- if let Some(user_data_dir) = &pending.webview_attributes.data_directory {
- if !user_data_dir.exists() {
- create_dir_all(user_data_dir)?;
- }
- }
- Ok(pending)
- }
- pub fn attach_window(
- &self,
- app_handle: AppHandle<R>,
- window: DetachedWindow<EventLoopMessage, R>,
- ) -> Window<R> {
- let window = Window::new(self.clone(), window, app_handle);
- let window_ = window.clone();
- let window_event_listeners = self.inner.window_event_listeners.clone();
- let manager = self.clone();
- window.on_window_event(move |event| {
- let _ = on_window_event(&window_, &manager, event);
- for handler in window_event_listeners.iter() {
- handler(GlobalWindowEvent {
- window: window_.clone(),
- event: event.clone(),
- });
- }
- });
- {
- let window_ = window.clone();
- let menu_event_listeners = self.inner.menu_event_listeners.clone();
- window.on_menu_event(move |event| {
- let _ = on_menu_event(&window_, &event);
- for handler in menu_event_listeners.iter() {
- handler(WindowMenuEvent {
- window: window_.clone(),
- menu_item_id: event.menu_item_id.clone(),
- });
- }
- });
- }
- // insert the window into our manager
- {
- self
- .windows_lock()
- .insert(window.label().to_string(), window.clone());
- }
- // let plugins know that a new window has been added to the manager
- let manager = self.inner.clone();
- let window_ = window.clone();
- // run on main thread so the plugin store doesn't dead lock with the event loop handler in App
- let _ = window.run_on_main_thread(move || {
- manager
- .plugins
- .lock()
- .expect("poisoned plugin store")
- .created(window_);
- });
- window
- }
- pub(crate) fn on_window_close(&self, label: &str) {
- self.windows_lock().remove(label);
- }
- pub fn emit_filter<S, F>(
- &self,
- event: &str,
- source_window_label: Option<&str>,
- payload: S,
- filter: F,
- ) -> crate::Result<()>
- where
- S: Serialize + Clone,
- F: Fn(&Window<R>) -> bool,
- {
- assert_event_name_is_valid(event);
- self
- .windows_lock()
- .values()
- .filter(|&w| filter(w))
- .try_for_each(|window| window.emit_internal(event, source_window_label, payload.clone()))
- }
- pub fn eval_script_all<S: Into<String>>(&self, script: S) -> crate::Result<()> {
- let script = script.into();
- self
- .windows_lock()
- .values()
- .try_for_each(|window| window.eval(&script))
- }
- pub fn labels(&self) -> HashSet<String> {
- self.windows_lock().keys().cloned().collect()
- }
- pub fn config(&self) -> Arc<Config> {
- self.inner.config.clone()
- }
- pub fn package_info(&self) -> &PackageInfo {
- &self.inner.package_info
- }
- pub fn unlisten(&self, handler_id: EventHandler) {
- self.inner.listeners.unlisten(handler_id)
- }
- pub fn trigger(&self, event: &str, window: Option<String>, data: Option<String>) {
- assert_event_name_is_valid(event);
- self.inner.listeners.trigger(event, window, data)
- }
- pub fn listen<F: Fn(Event) + Send + 'static>(
- &self,
- event: String,
- window: Option<String>,
- handler: F,
- ) -> EventHandler {
- assert_event_name_is_valid(&event);
- self.inner.listeners.listen(event, window, handler)
- }
- pub fn once<F: FnOnce(Event) + Send + 'static>(
- &self,
- event: String,
- window: Option<String>,
- handler: F,
- ) -> EventHandler {
- assert_event_name_is_valid(&event);
- self.inner.listeners.once(event, window, handler)
- }
- pub fn event_listeners_object_name(&self) -> String {
- self.inner.listeners.listeners_object_name()
- }
- pub fn event_emit_function_name(&self) -> String {
- self.inner.listeners.function_name()
- }
- pub fn get_window(&self, label: &str) -> Option<Window<R>> {
- self.windows_lock().get(label).cloned()
- }
- pub fn windows(&self) -> HashMap<String, Window<R>> {
- self.windows_lock().clone()
- }
- }
- /// Tray APIs
- #[cfg(all(desktop, feature = "system-tray"))]
- impl<R: Runtime> WindowManager<R> {
- pub fn get_tray(&self, id: &str) -> Option<crate::SystemTrayHandle<R>> {
- self.inner.trays.lock().unwrap().get(id).cloned()
- }
- pub fn trays(&self) -> HashMap<String, crate::SystemTrayHandle<R>> {
- self.inner.trays.lock().unwrap().clone()
- }
- pub fn attach_tray(&self, id: String, tray: crate::SystemTrayHandle<R>) {
- self.inner.trays.lock().unwrap().insert(id, tray);
- }
- pub fn get_tray_by_runtime_id(&self, id: u16) -> Option<(String, crate::SystemTrayHandle<R>)> {
- let trays = self.inner.trays.lock().unwrap();
- let iter = trays.iter();
- for (tray_id, tray) in iter {
- if tray.id == id {
- return Some((tray_id.clone(), tray.clone()));
- }
- }
- None
- }
- }
- fn on_window_event<R: Runtime>(
- window: &Window<R>,
- manager: &WindowManager<R>,
- event: &WindowEvent,
- ) -> crate::Result<()> {
- match event {
- WindowEvent::Resized(size) => window.emit(WINDOW_RESIZED_EVENT, size)?,
- WindowEvent::Moved(position) => window.emit(WINDOW_MOVED_EVENT, position)?,
- WindowEvent::CloseRequested { api } => {
- if window.has_js_listener(Some(window.label().into()), WINDOW_CLOSE_REQUESTED_EVENT) {
- api.prevent_close();
- }
- window.emit(WINDOW_CLOSE_REQUESTED_EVENT, ())?;
- }
- WindowEvent::Destroyed => {
- window.emit(WINDOW_DESTROYED_EVENT, ())?;
- let label = window.label();
- let windows_map = manager.inner.windows.lock().unwrap();
- let windows = windows_map.values();
- for window in windows {
- window.eval(&format!(
- r#"(function () {{ const metadata = window.__TAURI_METADATA__; if (metadata != null) {{ metadata.__windows = window.__TAURI_METADATA__.__windows.filter(w => w.label !== "{label}"); }} }})()"#
- ))?;
- }
- }
- WindowEvent::Focused(focused) => window.emit(
- if *focused {
- WINDOW_FOCUS_EVENT
- } else {
- WINDOW_BLUR_EVENT
- },
- (),
- )?,
- WindowEvent::ScaleFactorChanged {
- scale_factor,
- new_inner_size,
- ..
- } => window.emit(
- WINDOW_SCALE_FACTOR_CHANGED_EVENT,
- ScaleFactorChanged {
- scale_factor: *scale_factor,
- size: *new_inner_size,
- },
- )?,
- WindowEvent::FileDrop(event) => match event {
- FileDropEvent::Hovered(paths) => window.emit(WINDOW_FILE_DROP_HOVER_EVENT, paths)?,
- FileDropEvent::Dropped(paths) => {
- let scopes = window.state::<Scopes>();
- for path in paths {
- if path.is_file() {
- let _ = scopes.allow_file(path);
- } else {
- let _ = scopes.allow_directory(path, false);
- }
- }
- window.emit(WINDOW_FILE_DROP_EVENT, paths)?
- }
- FileDropEvent::Cancelled => window.emit(WINDOW_FILE_DROP_CANCELLED_EVENT, ())?,
- _ => unimplemented!(),
- },
- WindowEvent::ThemeChanged(theme) => window.emit(WINDOW_THEME_CHANGED, theme.to_string())?,
- }
- Ok(())
- }
- #[derive(Clone, Serialize)]
- #[serde(rename_all = "camelCase")]
- struct ScaleFactorChanged {
- scale_factor: f64,
- size: PhysicalSize<u32>,
- }
- fn on_menu_event<R: Runtime>(window: &Window<R>, event: &MenuEvent) -> crate::Result<()> {
- window.emit(MENU_EVENT, event.menu_item_id.clone())
- }
- #[cfg(feature = "isolation")]
- fn request_to_path(request: &tauri_runtime::http::Request, base_url: &str) -> String {
- let mut path = request
- .uri()
- .split(&['?', '#'][..])
- // ignore query string
- .next()
- .unwrap()
- .trim_start_matches(base_url)
- .to_string();
- if path.ends_with('/') {
- path.pop();
- }
- let path = percent_encoding::percent_decode(path.as_bytes())
- .decode_utf8_lossy()
- .to_string();
- if path.is_empty() {
- // if the url has no path, we should load `index.html`
- "index.html".to_string()
- } else {
- // skip leading `/`
- path.chars().skip(1).collect()
- }
- }
- #[cfg(test)]
- mod tests {
- use super::replace_with_callback;
- #[test]
- fn string_replace_with_callback() {
- let mut tauri_index = 0;
- #[allow(clippy::single_element_loop)]
- for (src, pattern, replacement, result) in [(
- "tauri is awesome, tauri is amazing",
- "tauri",
- || {
- tauri_index += 1;
- tauri_index.to_string()
- },
- "1 is awesome, 2 is amazing",
- )] {
- assert_eq!(replace_with_callback(src, pattern, replacement), result);
- }
- }
- }
|