diff --git a/.changes/base64.md b/.changes/base64.md new file mode 100644 index 000000000000..2fb599baba49 --- /dev/null +++ b/.changes/base64.md @@ -0,0 +1,5 @@ +--- +"tauri-macos-sign": patch:enhance +--- + +Do not rely on system base64 CLI to decode certificates. diff --git a/.changes/data-tauri-drag-region-deep.md b/.changes/data-tauri-drag-region-deep.md new file mode 100644 index 000000000000..1391ff5ac589 --- /dev/null +++ b/.changes/data-tauri-drag-region-deep.md @@ -0,0 +1,5 @@ +--- +"tauri": minor:feat +--- + +Add `data-tauri-drag-region="deep"` so clicks on non-clickable children will drag as well. Can still opt out of drag on some regions using `data-tauri-drag-region="false"` diff --git a/Cargo.lock b/Cargo.lock index ffa7c32f7962..8b9b0da50813 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8712,6 +8712,7 @@ name = "tauri-macos-sign" version = "2.3.3" dependencies = [ "apple-codesign", + "base64 0.22.1", "chrono", "dirs 6.0.0", "log", diff --git a/crates/tauri-macos-sign/Cargo.toml b/crates/tauri-macos-sign/Cargo.toml index af15c66fa8e5..fa4c429fd0f3 100644 --- a/crates/tauri-macos-sign/Cargo.toml +++ b/crates/tauri-macos-sign/Cargo.toml @@ -25,3 +25,4 @@ log = { version = "0.4.21", features = ["kv"] } apple-codesign = { version = "0.27", default-features = false } chrono = "0.4" p12 = "0.6" +base64 = "0.22" diff --git a/crates/tauri-macos-sign/src/lib.rs b/crates/tauri-macos-sign/src/lib.rs index 87e30fdeea62..c6e93821a66e 100644 --- a/crates/tauri-macos-sign/src/lib.rs +++ b/crates/tauri-macos-sign/src/lib.rs @@ -64,6 +64,8 @@ pub enum Error { }, #[error("failed to encode DER: {error}")] FailedToEncodeDER { error: std::io::Error }, + #[error("failed to decode base64 certificate: {0}")] + Base64Decode(base64::DecodeError), #[error("certificate missing common name")] CertificateMissingCommonName, #[error("certificate missing organization unit for common name {common_name}")] @@ -329,36 +331,23 @@ impl NotarytoolCmdExt for Command { } } -fn decode_base64(base64: &OsStr, out_path: &Path) -> Result<()> { - let tmp_dir = tempfile::tempdir().map_err(Error::TempDir)?; +fn decode_base64(base64_input: &OsStr, out_path: &Path) -> Result<()> { + use base64::Engine; - let src_path = tmp_dir.path().join("src"); - let base64 = base64 + let input = base64_input .to_str() - .expect("failed to convert base64 to string") - .as_bytes(); - - // as base64 contain whitespace decoding may be broken - // https://github.com/marshallpierce/rust-base64/issues/105 - // we'll use builtin base64 command from the OS - std::fs::write(&src_path, base64).map_err(|error| Error::Fs { - context: "failed to write base64 to temp file", - path: src_path.clone(), - error, - })?; + .expect("failed to convert base64 to string"); - assert_command( - std::process::Command::new("base64") - .arg("--decode") - .arg("-i") - .arg(&src_path) - .arg("-o") - .arg(out_path) - .piped(), - "failed to decode certificate", - ) - .map_err(|error| Error::CommandFailed { - command: "base64 --decode".to_string(), + // strip whitespace before decoding + let cleaned: String = input.chars().filter(|c| !c.is_ascii_whitespace()).collect(); + + let decoded = base64::engine::general_purpose::STANDARD + .decode(&cleaned) + .map_err(Error::Base64Decode)?; + + std::fs::write(out_path, &decoded).map_err(|error| Error::Fs { + context: "failed to write decoded certificate", + path: out_path.to_path_buf(), error, })?; diff --git a/crates/tauri/src/window/scripts/drag.js b/crates/tauri/src/window/scripts/drag.js index 1c9461b63d76..b22d46f20faa 100644 --- a/crates/tauri/src/window/scripts/drag.js +++ b/crates/tauri/src/window/scripts/drag.js @@ -3,30 +3,78 @@ // SPDX-License-Identifier: MIT ;(function () { - const osName = __TEMPLATE_os_name__ - //-----------------------// // drag on mousedown and maximize on double click on Windows and Linux - // while macOS macos maximization should be on mouseup and if the mouse + // while macOS maximization should be on mouseup and if the mouse // moves after the double click, it should be cancelled (see https://github.com/tauri-apps/tauri/issues/8306) //-----------------------// const TAURI_DRAG_REGION_ATTR = 'data-tauri-drag-region' + function isClickableElement(el) { + const tag = el.tagName && el.tagName.toLowerCase() + + return ( + tag === 'a' + || tag === 'button' + || tag === 'input' + || tag === 'select' + || tag === 'textarea' + || tag === 'label' + || tag === 'summary' + || (el.hasAttribute('contenteditable') + && el.getAttribute('contenteditable') !== 'false') + || (el.hasAttribute('tabindex') && el.getAttribute('tabindex') !== '-1') + ) + } + + // Walk the composed path from target upward. If a clickable element or a + // data-tauri-drag-region="false" element is encountered, return false (don't drag). + // Otherwise return true. + // + // Supported values for data-tauri-drag-region: + // (bare / no value) → self: only direct clicks on this element trigger drag + // "deep" → deep: clicks anywhere in the subtree trigger drag + // "false" → disabled: drag is blocked here (and for ancestors) + function isDragRegion(composedPath) { + for (const el of composedPath) { + if (!(el instanceof HTMLElement)) continue + + // if we hit a clickable element or a disabled drag region, don't drag + if ( + isClickableElement(el) + || el.getAttribute(TAURI_DRAG_REGION_ATTR) === 'false' + ) { + return false + } + + const attr = el.getAttribute(TAURI_DRAG_REGION_ATTR) + if (attr !== null) { + // deep: the whole subtree is a drag region + if (attr === 'deep') return true + // bare (or any unrecognized value): self-only + if (el === composedPath[0]) return true + // click was on a child of a self-only region — stop walking, don't drag + return false + } + } + + return false + } + + const osName = __TEMPLATE_os_name__ + // initial mousedown position for macOS let initialX = 0 let initialY = 0 document.addEventListener('mousedown', (e) => { - const attr = e.target.getAttribute(TAURI_DRAG_REGION_ATTR) if ( - // element has the magic data attribute - attr !== null - // and not false - && attr !== 'false' - // and was left mouse button - && e.button === 0 + // was left mouse button + e.button === 0 // and was normal click to drag or double click to maximize && (e.detail === 1 || e.detail === 2) + // and is drag region + && isDragRegion(e.composedPath()) ) { // macOS maximization happens on `mouseup`, // so we save needed state and early return @@ -48,23 +96,21 @@ window.__TAURI_INTERNALS__.invoke('plugin:window|' + cmd) } }) + // on macOS we maximize on mouseup instead, to match the system behavior where maximization can be canceled // if the mouse moves outside the data-tauri-drag-region if (osName === 'macos') { document.addEventListener('mouseup', (e) => { - const attr = e.target.getAttribute(TAURI_DRAG_REGION_ATTR) if ( - // element has the magic data attribute - attr !== null - // and not false - && attr !== 'false' - // and was left mouse button - && e.button === 0 + // was left mouse button + e.button === 0 // and was double click && e.detail === 2 // and the cursor hasn't moved from initial mousedown && e.clientX === initialX && e.clientY === initialY + // and the event path contains a drag region (with no clickable element in between) + && isDragRegion(e.composedPath()) ) { window.__TAURI_INTERNALS__.invoke( 'plugin:window|internal_toggle_maximize'