< 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
 2073static MIRROR: LazyLock<Mutex<Mirror>> = LazyLock::new(|| Mutex::new(Mirror::default()));
 74
 12075fn now_ms() -> i64 {
 12076    SystemTime::now()
 12077        .duration_since(UNIX_EPOCH)
 12078        .map(|since_epoch| since_epoch.as_millis() as i64)
 12079        .unwrap_or(0)
 12080}
 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).
 6084fn current_host(webview_id: u64) -> String {
 6085    qtcore::cookies::host_of_url(&crate::bridge::webview::ffi::qt_webview_url(webview_id))
 6086}
 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).
 24090fn with_webview<T>(webview_id: u64, f: impl FnOnce(&mut HashMap<Key, Snapshot>) -> T) -> Result<T> {
 24091    let mut mirror = MIRROR.lock();
 24092    match mirror.cookies.get_mut(&webview_id) {
 24093        Some(cookies) => Ok(f(cookies)),
 094        None => Err(Error::WindowNotFound),
 95    }
 24096}
 97
 98/// The native view pointer of a registered webview.
 8099fn view_ptr_of(webview_id: u64) -> Result<u64> {
 80100    MIRROR
 80101        .lock()
 80102        .view_ptrs
 80103        .get(&webview_id)
 80104        .copied()
 80105        .ok_or(Error::WindowNotFound)
 80106}
 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.
 20114pub(crate) fn register_webview(webview_id: u64, webview_ptr: u64) {
 20115    {
 20116        let mut mirror = MIRROR.lock();
 20117        mirror.view_ptrs.insert(webview_id, webview_ptr);
 20118        mirror.ptr_to_id.insert(webview_ptr, webview_id);
 20119        mirror.cookies.insert(webview_id, HashMap::new());
 20120    }
 20121    ffi::qt_cookies_watch(webview_ptr);
 20122    crate::qtrace!(
 123        "[tauri-runtime-qt] cookie mirror watching webview {webview_id} (view {webview_ptr:#x})"
 124    );
 20125}
 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.
 80130pub(crate) fn on_cookie_added(
 80131    webview_ptr: u64,
 80132    name: &str,
 80133    value: &str,
 80134    domain: &str,
 80135    path: &str,
 80136    secure: bool,
 80137    http_only: bool,
 80138    same_site: i32,
 80139    expires_ms: i64,
 80140) {
 80141    let mut mirror = MIRROR.lock();
 80142    let Some(&webview_id) = mirror.ptr_to_id.get(&webview_ptr) else {
 0143        return; // view never registered, or already destroyed
 144    };
 80145    let snapshot = Snapshot {
 80146        name: name.to_string(),
 80147        value: value.to_string(),
 80148        domain: domain.to_ascii_lowercase(),
 80149        path: path.to_string(),
 80150        secure,
 80151        http_only,
 80152        same_site: qtcore::cookies::same_site_from_code(same_site),
 80153        expires_ms: (expires_ms >= 0).then_some(expires_ms),
 80154    };
 80155    let key = snapshot.key();
 80156    let Some(cookies) = mirror.cookies.get_mut(&webview_id) else {
 0157        return;
 158    };
 80159    cookies.insert(key, snapshot);
 80160}
 161
 162/// A cookie left the store; removes the canonical key.
 80163pub(crate) fn on_cookie_removed(webview_ptr: u64, name: &str, domain: &str, path: &str) {
 80164    let mut mirror = MIRROR.lock();
 80165    let Some(&webview_id) = mirror.ptr_to_id.get(&webview_ptr) else {
 0166        return;
 167    };
 80168    if let Some(cookies) = mirror.cookies.get_mut(&webview_id) {
 80169        cookies.remove(&qtcore::cookies::key_of(name, domain, path));
 80170    }
 80171}
 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.
 140178pub(crate) fn cookies(webview_id: u64) -> Result<Vec<Cookie<'static>>> {
 140179    with_webview(webview_id, |cookies| {
 140180        let mut snapshots: Vec<&Snapshot> = cookies.values().collect();
 140181        qtcore::cookies::sort_by_identity(&mut snapshots);
 140182        snapshots
 140183            .into_iter()
 140184            .map(qtcore::cookies::to_public_cookie)
 140185            .collect()
 140186    })
 140187}
 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.
 80200pub(crate) fn cookies_for_url(webview_id: u64, url: &url::Url) -> Result<Vec<Cookie<'static>>> {
 80201    let now = now_ms();
 80202    with_webview(webview_id, |cookies| {
 80203        let snapshots: Vec<&Snapshot> = cookies.values().collect();
 80204        qtcore::cookies::cookies_for_url(&snapshots, url, now)
 80205    })
 80206}
 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.
 40213pub(crate) fn set_cookie(webview_id: u64, cookie: Cookie<'_>) -> Result<()> {
 40214    let view_ptr = view_ptr_of(webview_id)?;
 40215    let host = current_host(webview_id);
 40216    let domain = qtcore::cookies::canonical_domain(cookie.domain(), &host);
 40217    if domain.is_empty() {
 0218        return Err(Error::CreateWebview(
 0219            qtcore::cookies::missing_domain_message("set").into(),
 0220        ));
 40221    }
 40222    let name = cookie.name().to_string();
 40223    let value = cookie.value().to_string();
 40224    let path = qtcore::cookies::request_path_of(&cookie);
 40225    let secure = cookie.secure().unwrap_or(false);
 40226    let http_only = cookie.http_only().unwrap_or(false);
 40227    let same_site = cookie.same_site();
 40228    let expires_ms = qtcore::cookies::expires_ms_of(&cookie, now_ms());
 229
 40230    crate::qtrace!(
 231        "[tauri-runtime-qt] set_cookie -> view {view_ptr:#x} name={name:?} value={value:?} domain={domain:?} path={path:
 40232        qtcore::cookies::same_site_code(same_site)
 233    );
 40234    ffi::qt_cookies_set(
 40235        view_ptr,
 40236        &name,
 40237        &value,
 40238        &domain,
 40239        &path,
 40240        secure,
 40241        http_only,
 40242        qtcore::cookies::same_site_code(same_site),
 40243        expires_ms,
 244    );
 245
 40246    let snapshot = Snapshot {
 40247        name: name.clone(),
 40248        value,
 40249        domain: domain.clone(),
 40250        path: path.clone(),
 40251        secure,
 40252        http_only,
 40253        same_site,
 40254        expires_ms: (expires_ms >= 0).then_some(expires_ms),
 40255    };
 40256    let mut mirror = MIRROR.lock();
 40257    if let Some(cookies) = mirror.cookies.get_mut(&webview_id) {
 40258        cookies.insert(qtcore::cookies::key_of(&name, &domain, &path), snapshot);
 40259    }
 40260    Ok(())
 40261}
 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.
 20269pub(crate) fn delete_cookie(webview_id: u64, cookie: Cookie<'_>) -> Result<()> {
 20270    let view_ptr = view_ptr_of(webview_id)?;
 20271    let host = current_host(webview_id);
 20272    let domain = qtcore::cookies::canonical_domain(cookie.domain(), &host);
 20273    if domain.is_empty() {
 0274        return Err(Error::CreateWebview(
 0275            qtcore::cookies::missing_domain_message("delete").into(),
 0276        ));
 20277    }
 20278    let name = cookie.name().to_string();
 20279    let request_path = qtcore::cookies::request_path_of(&cookie);
 280
 20281    ffi::qt_cookies_delete(view_ptr, &name, &domain, &request_path);
 282
 20283    let mut mirror = MIRROR.lock();
 20284    if let Some(cookies) = mirror.cookies.get_mut(&webview_id) {
 40285        cookies.retain(|(key_name, key_domain, key_path), _| {
 40286            !(key_name == &name
 20287                && qtcore::cookies::domain_matches(key_domain.as_str(), &domain)
 20288                && qtcore::cookies::path_matches(key_path.as_str(), &request_path))
 40289        });
 0290    }
 20291    Ok(())
 20292}
 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.
 20304pub(crate) fn clear_all_browsing_data(webview_id: u64) -> Result<()> {
 20305    let view_ptr = view_ptr_of(webview_id)?;
 20306    ffi::qt_cookies_clear_all(view_ptr);
 20307    with_webview(webview_id, |cookies| cookies.clear())
 20308}

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_