< Summary

Information
Class: /home/runner/work/TauriQt/TauriQt/tauri-runtime-qt/src/ops/cookies.rs
Assembly: Default
File(s): /home/runner/work/TauriQt/TauriQt/tauri-runtime-qt/src/ops/cookies.rs
Line coverage
93%
Covered lines: 148
Uncovered lines: 11
Coverable lines: 159
Total lines: 308
Line coverage: 93%
Branch coverage
N/A
Covered branches: 0
Total branches: 0
Branch coverage: N/A
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

File(s)

/home/runner/work/TauriQt/TauriQt/tauri-runtime-qt/src/ops/cookies.rs

#LineLine coverage
 1// SPDX-FileCopyrightText: 2026 Alper Çelik
 2// SPDX-License-Identifier: MIT OR Apache-2.0
 3
 4//! Cookie mirror (owned by the cookies worker).
 5//!
 6//! Why a copy instead of asking Qt: `QWebEngineCookieStore` can only enumerate
 7//! asynchronously (`loadAllCookies()`) and **never signals completion** — it
 8//! just emits one `cookieAdded` per cookie. The trait's `cookies()` is
 9//! synchronous and our scheme handler runs synchronously on the GUI thread, so
 10//! blocking on a nested event loop is not an option (re-entrancy + deadlock).
 11//! Instead we subscribe to `cookieAdded` / `cookieRemoved` (Qt does the
 12//! decryption and normalisation for us) and answer reads from this mirror.
 13//!
 14//! Semantics for callers: the mirror is an **observed snapshot**.
 15//! * An empty mirror is a legitimate "no cookies observed" answer, not an error.
 16//! * Cookies whose only change was server-side expiry, or cookies that were in
 17//!   the store before `qt_cookies_watch` ran (i.e. before the webview was
 18//!   created), appear only after the initial `loadAllCookies()` / navigation.
 19//! * `set_cookie` / `delete_cookie` update the mirror optimistically so a write
 20//!   is visible to the very next read. The authoritative `cookieAdded` /
 21//!   `cookieRemoved` event for the same name/domain/path then overwrites or
 22//!   removes that entry (idempotent upsert/remove, so event order does not
 23//!   matter). A write Chromium rejects outright (e.g. `SameSite=None` without
 24//!   `Secure`, or a domain that cannot be set from the synthesised origin) fires
 25//!   no event, so its provisional entry stays until something replaces it.
 26//! * Events for a webview that was never registered (or whose view was already
 27//!   destroyed) are ignored.
 28//!
 29//! What the Qt layer can and cannot carry (verified against Qt 6.11.2's
 30//! `QNetworkCookie` / cookie-store code):
 31//! * The domain of an event is Chromium's canonical form: a bare host for a
 32//!   host-only cookie, a dot-prefixed domain (".example.com") for a domain
 33//!   cookie, and the lowercased host for IP literals.
 34//!   `tauri-runtime-qt-core`'s `canonical_domain` reproduces that rule for
 35//!   optimistic writes.
 36//! * `same_site` is lossy in **both** directions through Qt: the
 37//!   `CanonicalCookie` -> `QNetworkCookie` conversion used for the signals never
 38//!   fills `sameSitePolicy()` (so events always report `Default`), and a write
 39//!   that carries a policy is overwritten by that same Default value as soon as
 40//!   the event arrives. The mirror therefore reports what Qt reports.
 41//! * CHIPS (`Cookie::partitioned()`) has no equivalent in the Qt API: the flag
 42//!   is dropped and the cookie is stored unpartitioned.
 43//! * `clear_all_browsing_data` is broader than cookies (HTTP cache, and by
 44//!   contract tauri's "all browsing data"); `clear_all_browsing_data()` here
 45//!   clears cookies + HTTP cache and documents the rest.
 46
 47use std::collections::HashMap;
 48use std::sync::LazyLock;
 49use std::time::{SystemTime, UNIX_EPOCH};
 50
 51use parking_lot::Mutex;
 52use tauri_runtime::{Cookie, Error, Result};
 53use tauri_runtime_qt_core as qtcore;
 54use tauri_runtime_qt_core::cookies::{Key, Snapshot};
 55
 56use crate::bridge::cookies::ffi;
 57
 58// `Snapshot` (one cookie as the Qt store reported it, or as a write asked for
 59// it) and `Key` (name + domain + path — Chromium's identity for a cookie) live
 60// in the core crate, together with every matching rule used below.
 61
 62/// The per-process mirror.
 63#[derive(Default)]
 64struct Mirror {
 65    /// webview id -> observed cookies.
 66    cookies: HashMap<u64, HashMap<Key, Snapshot>>,
 67    /// webview id -> native `QWebEngineView*`, for the FFI calls.
 68    view_ptrs: HashMap<u64, u64>,
 69    /// native pointer -> webview id; the only handle Qt's signals carry.
 70    ptr_to_id: HashMap<u64, u64>,
 71}
 72
 2173static MIRROR: LazyLock<Mutex<Mirror>> = LazyLock::new(|| Mutex::new(Mirror::default()));
 74
 12675fn now_ms() -> i64 {
 12676    SystemTime::now()
 12677        .duration_since(UNIX_EPOCH)
 12678        .map(|since_epoch| since_epoch.as_millis() as i64)
 12679        .unwrap_or(0)
 12680}
 81
 82/// The webview's current URL host, via the existing webview bridge ("" when the
 83/// webview has no page URL yet, e.g. before its first navigation).
 6384fn current_host(webview_id: u64) -> String {
 6385    qtcore::cookies::host_of_url(&crate::bridge::webview::ffi::qt_webview_url(webview_id))
 6386}
 87
 88/// Runs `f` on the mirror of `webview_id`, or fails when the webview was never
 89/// registered (unknown handle -> `WindowNotFound`, like the other ops).
 25290fn with_webview<T>(webview_id: u64, f: impl FnOnce(&mut HashMap<Key, Snapshot>) -> T) -> Result<T> {
 25291    let mut mirror = MIRROR.lock();
 25292    match mirror.cookies.get_mut(&webview_id) {
 25293        Some(cookies) => Ok(f(cookies)),
 094        None => Err(Error::WindowNotFound),
 95    }
 25296}
 97
 98/// The native view pointer of a registered webview.
 8499fn view_ptr_of(webview_id: u64) -> Result<u64> {
 84100    MIRROR
 84101        .lock()
 84102        .view_ptrs
 84103        .get(&webview_id)
 84104        .copied()
 84105        .ok_or(Error::WindowNotFound)
 84106}
 107
 108/// Starts mirroring the cookie store of the given webview.
 109///
 110/// Never returns an error because it runs inside webview creation: the view is
 111/// recorded either way, so later reads answer "no cookies observed" instead of
 112/// failing window creation. The C++ side logs nothing; a failure to watch
 113/// (unknown pointer, no cookie store) simply leaves the mirror empty.
 21114pub(crate) fn register_webview(webview_id: u64, webview_ptr: u64) {
 21115    {
 21116        let mut mirror = MIRROR.lock();
 21117        mirror.view_ptrs.insert(webview_id, webview_ptr);
 21118        mirror.ptr_to_id.insert(webview_ptr, webview_id);
 21119        mirror.cookies.insert(webview_id, HashMap::new());
 21120    }
 21121    ffi::qt_cookies_watch(webview_ptr);
 21122    crate::qtrace!(
 123        "[tauri-runtime-qt] cookie mirror watching webview {webview_id} (view {webview_ptr:#x})"
 124    );
 21125}
 126
 127/// A cookie entered the store (including the initial load). Called from the Qt
 128/// signal; the entry is upserted under Chromium's canonical key, so a repeated
 129/// `loadAllCookies()` or an event after an optimistic write is idempotent.
 84130pub(crate) fn on_cookie_added(
 84131    webview_ptr: u64,
 84132    name: &str,
 84133    value: &str,
 84134    domain: &str,
 84135    path: &str,
 84136    secure: bool,
 84137    http_only: bool,
 84138    same_site: i32,
 84139    expires_ms: i64,
 84140) {
 84141    let mut mirror = MIRROR.lock();
 84142    let Some(&webview_id) = mirror.ptr_to_id.get(&webview_ptr) else {
 0143        return; // view never registered, or already destroyed
 144    };
 84145    let snapshot = Snapshot {
 84146        name: name.to_string(),
 84147        value: value.to_string(),
 84148        domain: domain.to_ascii_lowercase(),
 84149        path: path.to_string(),
 84150        secure,
 84151        http_only,
 84152        same_site: qtcore::cookies::same_site_from_code(same_site),
 84153        expires_ms: (expires_ms >= 0).then_some(expires_ms),
 84154    };
 84155    let key = snapshot.key();
 84156    let Some(cookies) = mirror.cookies.get_mut(&webview_id) else {
 0157        return;
 158    };
 84159    cookies.insert(key, snapshot);
 84160}
 161
 162/// A cookie left the store; removes the canonical key.
 84163pub(crate) fn on_cookie_removed(webview_ptr: u64, name: &str, domain: &str, path: &str) {
 84164    let mut mirror = MIRROR.lock();
 84165    let Some(&webview_id) = mirror.ptr_to_id.get(&webview_ptr) else {
 0166        return;
 167    };
 84168    if let Some(cookies) = mirror.cookies.get_mut(&webview_id) {
 84169        cookies.remove(&qtcore::cookies::key_of(name, domain, path));
 84170    }
 84171}
 172
 173// ---- trait surface ----
 174
 175/// Every cookie the mirror has observed for this webview, sorted by
 176/// (domain, path, name) for deterministic output. Expired entries can appear
 177/// here (an expiry-only change emits no event); `cookies_for_url` filters them.
 147178pub(crate) fn cookies(webview_id: u64) -> Result<Vec<Cookie<'static>>> {
 147179    with_webview(webview_id, |cookies| {
 147180        let mut snapshots: Vec<&Snapshot> = cookies.values().collect();
 147181        qtcore::cookies::sort_by_identity(&mut snapshots);
 147182        snapshots
 147183            .into_iter()
 147184            .map(qtcore::cookies::to_public_cookie)
 147185            .collect()
 147186    })
 147187}
 188
 189/// The cookies a request to `url` would carry, matching Chromium's rules:
 190///
 191/// * a host-only cookie (domain without a leading dot) matches the exact host;
 192/// * a domain cookie (leading dot) matches that host and every subdomain;
 193/// * the cookie's path must be a prefix of the request path (empty -> "/");
 194/// * `Secure` cookies are returned for `https`/`wss` requests only;
 195/// * expired cookies are skipped.
 196///
 197/// Deliberate difference from Chromium: the scheme gate (Chromium only answers
 198/// for `http`/`https`/`ws`/`wss`) is not applied, so callers can ask about the
 199/// runtime's own `tauri://` pages; host and path still decide the match.
 84200pub(crate) fn cookies_for_url(webview_id: u64, url: &url::Url) -> Result<Vec<Cookie<'static>>> {
 84201    let now = now_ms();
 84202    with_webview(webview_id, |cookies| {
 84203        let snapshots: Vec<&Snapshot> = cookies.values().collect();
 84204        qtcore::cookies::cookies_for_url(&snapshots, url, now)
 84205    })
 84206}
 207
 208/// Writes a cookie through Qt and records it optimistically.
 209///
 210/// A cookie without a domain becomes host-only for the webview's current page
 211/// host; before the first navigation there is no host to attach it to, which is
 212/// an error rather than a silently dropped write.
 42213pub(crate) fn set_cookie(webview_id: u64, cookie: Cookie<'_>) -> Result<()> {
 42214    let view_ptr = view_ptr_of(webview_id)?;
 42215    let host = current_host(webview_id);
 42216    let domain = qtcore::cookies::canonical_domain(cookie.domain(), &host);
 42217    if domain.is_empty() {
 0218        return Err(Error::CreateWebview(
 0219            qtcore::cookies::missing_domain_message("set").into(),
 0220        ));
 42221    }
 42222    let name = cookie.name().to_string();
 42223    let value = cookie.value().to_string();
 42224    let path = qtcore::cookies::request_path_of(&cookie);
 42225    let secure = cookie.secure().unwrap_or(false);
 42226    let http_only = cookie.http_only().unwrap_or(false);
 42227    let same_site = cookie.same_site();
 42228    let expires_ms = qtcore::cookies::expires_ms_of(&cookie, now_ms());
 229
 42230    crate::qtrace!(
 231        "[tauri-runtime-qt] set_cookie -> view {view_ptr:#x} name={name:?} value={value:?} domain={domain:?} path={path:
 42232        qtcore::cookies::same_site_code(same_site)
 233    );
 42234    ffi::qt_cookies_set(
 42235        view_ptr,
 42236        &name,
 42237        &value,
 42238        &domain,
 42239        &path,
 42240        secure,
 42241        http_only,
 42242        qtcore::cookies::same_site_code(same_site),
 42243        expires_ms,
 244    );
 245
 42246    let snapshot = Snapshot {
 42247        name: name.clone(),
 42248        value,
 42249        domain: domain.clone(),
 42250        path: path.clone(),
 42251        secure,
 42252        http_only,
 42253        same_site,
 42254        expires_ms: (expires_ms >= 0).then_some(expires_ms),
 42255    };
 42256    let mut mirror = MIRROR.lock();
 42257    if let Some(cookies) = mirror.cookies.get_mut(&webview_id) {
 42258        cookies.insert(qtcore::cookies::key_of(&name, &domain, &path), snapshot);
 42259    }
 42260    Ok(())
 42261}
 262
 263/// Deletes cookies through Qt and removes them from the mirror optimistically.
 264///
 265/// Qt builds Chromium's deletion filter from the cookie *name* plus a URL
 266/// derived from the domain, so every path variant of that name for the host is
 267/// deleted; the optimistic removal mirrors that (name + domain match + path
 268/// prefix) rather than only the exact key.
 21269pub(crate) fn delete_cookie(webview_id: u64, cookie: Cookie<'_>) -> Result<()> {
 21270    let view_ptr = view_ptr_of(webview_id)?;
 21271    let host = current_host(webview_id);
 21272    let domain = qtcore::cookies::canonical_domain(cookie.domain(), &host);
 21273    if domain.is_empty() {
 0274        return Err(Error::CreateWebview(
 0275            qtcore::cookies::missing_domain_message("delete").into(),
 0276        ));
 21277    }
 21278    let name = cookie.name().to_string();
 21279    let request_path = qtcore::cookies::request_path_of(&cookie);
 280
 21281    ffi::qt_cookies_delete(view_ptr, &name, &domain, &request_path);
 282
 21283    let mut mirror = MIRROR.lock();
 21284    if let Some(cookies) = mirror.cookies.get_mut(&webview_id) {
 42285        cookies.retain(|(key_name, key_domain, key_path), _| {
 42286            !(key_name == &name
 21287                && qtcore::cookies::domain_matches(key_domain.as_str(), &domain)
 21288                && qtcore::cookies::path_matches(key_path.as_str(), &request_path))
 42289        });
 0290    }
 21291    Ok(())
 21292}
 293
 294/// Clears the browsing data Qt 6.11 can clear for this webview: the HTTP cache
 295/// and every cookie of the profile. The cookie mirror is emptied in the same
 296/// call — Qt's `deleteAllCookies()` is asynchronous and has no completion
 297/// signal, so waiting for the `cookieRemoved` events would race; the events
 298/// that do arrive find nothing to remove.
 299///
 300/// NOT cleared (Qt exposes no live-profile API for them): localStorage /
 301/// sessionStorage, IndexedDB, service-worker registrations and Cache Storage.
 302/// They live under the profile's storage directory, which this runtime does not
 303/// delete while the profile is alive.
 21304pub(crate) fn clear_all_browsing_data(webview_id: u64) -> Result<()> {
 21305    let view_ptr = view_ptr_of(webview_id)?;
 21306    ffi::qt_cookies_clear_all(view_ptr);
 21307    with_webview(webview_id, |cookies| cookies.clear())
 21308}

Methods/Properties

_RNCNvNtNtCsdgr7Em4UC3i_16tauri_runtime_qt3ops7cookies6MIRROR0B7_
_RNvNtNtCsdgr7Em4UC3i_16tauri_runtime_qt3ops7cookies6now_ms
_RNCNvNtNtCsdgr7Em4UC3i_16tauri_runtime_qt3ops7cookies6now_ms0B7_
_RNvNtNtCsdgr7Em4UC3i_16tauri_runtime_qt3ops7cookies12current_host
_RINvNtNtCsdgr7Em4UC3i_16tauri_runtime_qt3ops7cookies12with_webviewINtNtCsfVYBsOggKhj_5alloc3vec3VecNtCsdPxPIdRrFkl_6cookie6CookieENCNvB2_15cookies_for_url0EB6_
_RINvNtNtCsdgr7Em4UC3i_16tauri_runtime_qt3ops7cookies12with_webviewINtNtCsfVYBsOggKhj_5alloc3vec3VecNtCsdPxPIdRrFkl_6cookie6CookieENCNvB2_7cookies0EB6_
_RINvNtNtCsdgr7Em4UC3i_16tauri_runtime_qt3ops7cookies12with_webviewuNCNvB2_23clear_all_browsing_data0EB6_
_RNvNtNtCsdgr7Em4UC3i_16tauri_runtime_qt3ops7cookies11view_ptr_of
_RNvNtNtCsdgr7Em4UC3i_16tauri_runtime_qt3ops7cookies16register_webview
_RNvNtNtCsdgr7Em4UC3i_16tauri_runtime_qt3ops7cookies15on_cookie_added
_RNvNtNtCsdgr7Em4UC3i_16tauri_runtime_qt3ops7cookies17on_cookie_removed
_RNvNtNtCsdgr7Em4UC3i_16tauri_runtime_qt3ops7cookies7cookies
_RNCNvNtNtCsdgr7Em4UC3i_16tauri_runtime_qt3ops7cookies7cookies0B7_
_RNvNtNtCsdgr7Em4UC3i_16tauri_runtime_qt3ops7cookies15cookies_for_url
_RNCNvNtNtCsdgr7Em4UC3i_16tauri_runtime_qt3ops7cookies15cookies_for_url0B7_
_RNvNtNtCsdgr7Em4UC3i_16tauri_runtime_qt3ops7cookies10set_cookie
_RNvNtNtCsdgr7Em4UC3i_16tauri_runtime_qt3ops7cookies13delete_cookie
_RNCNvNtNtCsdgr7Em4UC3i_16tauri_runtime_qt3ops7cookies13delete_cookie0B7_
_RNvNtNtCsdgr7Em4UC3i_16tauri_runtime_qt3ops7cookies23clear_all_browsing_data
_RNCNvNtNtCsdgr7Em4UC3i_16tauri_runtime_qt3ops7cookies23clear_all_browsing_data0B7_