< Summary

Information
Class: /home/runner/work/TauriQt/TauriQt/tauri-runtime-qt/src/qt_cookies.cpp
Assembly: Default
File(s): /home/runner/work/TauriQt/TauriQt/tauri-runtime-qt/src/qt_cookies.cpp
Line coverage
68%
Covered lines: 121
Uncovered lines: 56
Coverable lines: 177
Total lines: 317
Line coverage: 68.3%
Branch coverage
46%
Covered branches: 35
Total branches: 76
Branch coverage: 46%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

File(s)

/home/runner/work/TauriQt/TauriQt/tauri-runtime-qt/src/qt_cookies.cpp

#LineLine coverage
 1// SPDX-FileCopyrightText: 2026 Alper Çelik
 2// SPDX-License-Identifier: MIT OR Apache-2.0
 3//
 4// Cookie store access for the Qt runtime (mirroring, writes, deletion, clear).
 5//
 6// THREADING: every function here runs on the Qt GUI thread (the thread that
 7// created the QApplication). `QWebEngineView` / `QWebEnginePage` /
 8// `QWebEngineProfile` / `QWebEngineCookieStore` are GUI-thread-affine, and the
 9// Rust side only calls these from the dispatchers tauri invokes on that thread.
 10// The cookie signals are documented as *not* a place for heavy work (they block
 11// Chromium's IO thread while a blocking connection runs); the forwarding below
 12// is a few map lookups plus one Rust call each, so it stays cheap.
 13//
 14// HANDLE CONTRACT: `webview_ptr` is the value returned by
 15// `qt_webview_native_handle()` — the address of a `QWebEngineView` created by
 16// `qt_webview_create()`. It is cast back with `reinterpret_cast`. A null or
 17// unknown pointer makes every function here a no-op (the caller decided the
 18// handle is valid when it registered the view; the C++ side must not throw
 19// inside webview creation).
 20//
 21// WHAT QT REPORTS (verified against Qt 6.11.2 / its Chromium 140):
 22//   * `cookieAdded` / `cookieRemoved` carry the *canonical* cookie Chromium
 23//     stored: a host-only cookie keeps its bare host as the domain, a domain
 24//     cookie keeps the leading dot (".example.com"), and the path is never
 25//     empty ("/" at minimum).
 26//   * `QWebEngineCookieStore::setCookie()` takes a `QNetworkCookie` and turns it
 27//     into a `Set-Cookie` line (`QNetworkCookie::toRawForm()`), which Chromium
 28//     parses against a URL it synthesises from the cookie's own domain — so a
 29//     write works from any page scheme, including the runtime's custom
 30//     `tauri://` scheme. A *dotless* domain attribute is made a domain cookie by
 31//     Chromium (it prepends the dot) unless the host has no registrable domain
 32//     (`localhost`, IP literals), in which case it stays host-only.
 33//   * `QWebEngineCookieStore::deleteCookie()` builds Chromium's deletion filter
 34//     from the cookie *name* plus a URL derived from the domain — the cookie's
 35//     path is NOT part of the filter, so a delete removes every path variant of
 36//     that name for the host.
 37//   * Qt 6.11 never fills `QNetworkCookie::sameSitePolicy()` when converting a
 38//     stored cookie back for a signal (`type_conversion.h` drops it), so the
 39//     callbacks below always report `Default` (0) for events. The mapping is
 40//     still implemented for the values the enum defines.
 41//   * `loadAllCookies()` emits one `cookieAdded` per stored cookie and never
 42//     signals completion — which is exactly why the Rust side keeps a mirror.
 43
 44#include "qt_cookies.hpp"
 45
 46// cxx puts the `extern "Rust"` callbacks declared in the bridge module into the
 47// generated header; never hand-declare them here.
 48#include "tauri-runtime-qt/src/bridge/cookies.rs.h"
 49
 50#include <QtCore/QByteArray>
 51#include <QtCore/QDateTime>
 52#include <QtCore/QObject>
 53#include <QtCore/QString>
 54#include <QtCore/QUrl>
 55#include <QtNetwork/QNetworkCookie>
 56#include <QtWebEngineCore/QWebEngineCookieStore>
 57#include <QtWebEngineCore/QWebEnginePage>
 58#include <QtWebEngineCore/QWebEngineProfile>
 59#include <QtWebEngineWidgets/QWebEngineView>
 60
 61#include <cstddef>
 62#include <cstdint>
 63#include <unordered_map>
 64#include <vector>
 65
 66namespace {
 67
 68/// Views whose stores are mirrored, by view pointer.
 69std::unordered_map<std::uint64_t, QWebEngineCookieStore*> g_view_stores;
 70/// Every view watching a store. A key's presence means the store's signals are
 71/// connected (the vector may be empty after a view is destroyed).
 72std::unordered_map<QWebEngineCookieStore*, std::vector<std::uint64_t>> g_store_views;
 73
 74/// `QNetworkCookie` value -> the bridge's `same_site` code.
 8075std::int32_t same_site_code(QNetworkCookie::SameSite policy) {
 8076    switch (policy) {
 077    case QNetworkCookie::SameSite::None:
 078        return 1;
 079    case QNetworkCookie::SameSite::Lax:
 080        return 2;
 081    case QNetworkCookie::SameSite::Strict:
 082        return 3;
 8083    case QNetworkCookie::SameSite::Default:
 8084        break;
 8085    }
 8086    return 0;
 8087}
 88
 89/// `rust::Str` view over a byte array that stays alive for the call.
 56090rust::Str str_of(const QByteArray& bytes) {
 56091    return rust::Str(bytes.constData(), static_cast<std::size_t>(bytes.size()));
 56092}
 93
 94/// `rust::Str` bytes as a `QByteArray` (both are UTF-8 by contract).
 10095QByteArray to_qbytearray(rust::Str s) {
 10096    return QByteArray(s.data(), static_cast<qsizetype>(s.size()));
 10097}
 98
 12099QString to_qstring(rust::Str s) {
 120100    return QString::fromUtf8(s.data(), static_cast<qsizetype>(s.size()));
 120101}
 102
 40103QNetworkCookie::SameSite same_site_from_code(std::int32_t code) {
 40104    switch (code) {
 40105    case 1:
 40106        return QNetworkCookie::SameSite::None;
 0107    case 2:
 0108        return QNetworkCookie::SameSite::Lax;
 0109    case 3:
 0110        return QNetworkCookie::SameSite::Strict;
 0111    default:
 0112        return QNetworkCookie::SameSite::Default;
 40113    }
 40114}
 115
 100116QWebEngineCookieStore* store_of(QWebEngineView* view) {
 100117    if (view == nullptr || view->page() == nullptr) {
 0118        return nullptr;
 0119    }
 100120    QWebEngineProfile* profile = view->page()->profile();
 100121    return profile != nullptr ? profile->cookieStore() : nullptr;
 100122}
 123
 100124QWebEngineView* view_of(std::uint64_t webview_ptr) {
 100125    return reinterpret_cast<QWebEngineView*>(webview_ptr);
 100126}
 127
 128/// Forwards one store event to every Rust mirror watching that store.
 160129void forward_cookie(QWebEngineCookieStore* store, const QNetworkCookie& cookie, bool removed) {
 160130    auto it = g_store_views.find(store);
 160131    if (it == g_store_views.end() || it->second.empty()) {
 0132        return;
 0133    }
 160134    const QByteArray name = cookie.name();
 160135    const QByteArray domain = cookie.domain().toUtf8();
 160136    const QByteArray path = cookie.path().toUtf8();
 137
 160138    if (removed) {
 80139        for (const std::uint64_t view_ptr : it->second) {
 80140            qt_on_cookie_removed(view_ptr, str_of(name), str_of(domain), str_of(path));
 80141        }
 80142        return;
 80143    }
 144
 80145    const QByteArray value = cookie.value();
 80146    const bool secure = cookie.isSecure();
 80147    const bool http_only = cookie.isHttpOnly();
 80148    const std::int32_t same_site = same_site_code(cookie.sameSitePolicy());
 80149    const std::int64_t expires_ms = cookie.isSessionCookie()
 80150        ? -1
 80151        : static_cast<std::int64_t>(cookie.expirationDate().toMSecsSinceEpoch());
 80152    for (const std::uint64_t view_ptr : it->second) {
 80153        qt_on_cookie_added(view_ptr, str_of(name), str_of(value), str_of(domain), str_of(path),
 80154                           secure, http_only, same_site, expires_ms);
 80155    }
 80156}
 157
 158} // namespace
 159
 20160void qt_cookies_watch(std::uint64_t webview_ptr) {
 20161    QWebEngineView* view = view_of(webview_ptr);
 20162    if (view == nullptr || g_view_stores.count(webview_ptr) != 0) {
 0163        return; // unknown handle, or already watching this pointer
 0164    }
 20165    QWebEngineCookieStore* store = store_of(view);
 20166    if (store == nullptr) {
 0167        return;
 0168    }
 169
 20170    g_view_stores.emplace(webview_ptr, store);
 20171    std::vector<std::uint64_t>& watchers = g_store_views[store];
 20172    const bool first_watcher = watchers.empty();
 20173    watchers.push_back(webview_ptr);
 174
 175    // One connection per store: the default profile's store is shared by every
 176    // view the runtime creates, so connecting per view would deliver every
 177    // event once per connected view.
 20178    if (first_watcher) {
 20179        QObject::connect(store, &QWebEngineCookieStore::cookieAdded, store,
 80180                         [store](const QNetworkCookie& cookie) {
 80181                             forward_cookie(store, cookie, false);
 80182                         });
 20183        QObject::connect(store, &QWebEngineCookieStore::cookieRemoved, store,
 80184                         [store](const QNetworkCookie& cookie) {
 80185                             forward_cookie(store, cookie, true);
 80186                         });
 20187    }
 188
 189    // Drop the registration with the view so its address can be recycled by a
 190    // later view without leaking events into the old mirror. No context object:
 191    // the functor must run *during* the view's destruction (it only touches this
 192    // file's registries, never the dying view).
 20193    QObject::connect(view, &QObject::destroyed, [webview_ptr]() {
 0194        auto entry = g_view_stores.find(webview_ptr);
 0195        if (entry == g_view_stores.end()) {
 0196            return;
 0197        }
 0198        QWebEngineCookieStore* store = entry->second;
 0199        g_view_stores.erase(entry);
 0200        auto watchers = g_store_views.find(store);
 0201        if (watchers == g_store_views.end()) {
 0202            return;
 0203        }
 0204        std::vector<std::uint64_t>& list = watchers->second;
 0205        for (auto it = list.begin(); it != list.end(); ++it) {
 0206            if (*it == webview_ptr) {
 0207                list.erase(it);
 0208                break;
 0209            }
 0210        }
 0211    });
 212
 213    // Deterministic fill: emits `cookieAdded` for every cookie the store holds
 214    // (no completion signal, hence the Rust mirror). Repeating it for a second
 215    // view on the same store is harmless — the mirror is keyed by
 216    // name/domain/path and treats an add as an upsert.
 20217    store->loadAllCookies();
 20218}
 219
 220void qt_cookies_set(std::uint64_t webview_ptr,
 221                    rust::Str name,
 222                    rust::Str value,
 223                    rust::Str domain,
 224                    rust::Str path,
 225                    bool secure,
 226                    bool http_only,
 227                    std::int32_t same_site,
 40228                    std::int64_t expires_ms) {
 40229    QWebEngineView* view = view_of(webview_ptr);
 40230    QWebEngineCookieStore* store = store_of(view);
 40231    if (store == nullptr) {
 0232        return;
 0233    }
 234
 40235    QNetworkCookie cookie(to_qbytearray(name), to_qbytearray(value));
 40236    const QString domain_str = to_qstring(domain);
 40237    const QString path_str = to_qstring(path);
 40238    if (!path_str.isEmpty()) {
 40239        cookie.setPath(path_str);
 40240    }
 40241    cookie.setSecure(secure);
 40242    cookie.setHttpOnly(http_only);
 40243    if (same_site != 0) {
 40244        cookie.setSameSitePolicy(same_site_from_code(same_site));
 40245    }
 40246    if (expires_ms >= 0) {
 247        // `expires_ms == -1` (session cookie) leaves the date invalid, which is
 248        // what `isSessionCookie()` and `toRawForm()` key on.
 0249        cookie.setExpirationDate(QDateTime::fromMSecsSinceEpoch(expires_ms));
 0250    }
 251
 40252    if (!domain_str.isEmpty()) {
 253        // Domain variant. `setCookie()` without an origin lets Chromium build
 254        // the source URL from `domain + path`, which is what makes the write
 255        // valid from a custom-scheme page. Chromium normalises this to
 256        // ".domain" (domain cookie) when the host has a registrable domain, and
 257        // keeps it bare (host-only) for `localhost` / IP literals — the Rust
 258        // mirror normalises its optimistic key the same way.
 40259        cookie.setDomain(domain_str);
 40260        store->setCookie(cookie);
 40261        return;
 40262    }
 263
 264    // Host-only variant: no domain attribute, the origin URL decides the host.
 265    // (A cookie with neither a domain nor an origin serialises to a URL like
 266    // "http:///" and is dropped by Chromium, so the view's host is used.)
 0267    const QUrl page_url = view->url();
 0268    if (page_url.host().isEmpty()) {
 0269        return;
 0270    }
 0271    QUrl origin;
 0272    origin.setScheme(secure ? QStringLiteral("https") : QStringLiteral("http"));
 0273    origin.setHost(page_url.host());
 0274    origin.setPath(QStringLiteral("/"));
 0275    store->setCookie(cookie, origin);
 0276}
 277
 278void qt_cookies_delete(std::uint64_t webview_ptr,
 279                       rust::Str name,
 280                       rust::Str domain,
 20281                       rust::Str path) {
 20282    QWebEngineCookieStore* store = store_of(view_of(webview_ptr));
 20283    if (store == nullptr) {
 0284        return;
 0285    }
 20286    QNetworkCookie cookie(to_qbytearray(name), QByteArray());
 20287    const QString domain_str = to_qstring(domain);
 20288    if (!domain_str.isEmpty()) {
 289        // Chromium's deletion filter is built from the name plus the URL
 290        // synthesised from the domain; the path does not restrict the filter.
 20291        cookie.setDomain(domain_str);
 20292    }
 20293    const QString path_str = to_qstring(path);
 20294    if (!path_str.isEmpty()) {
 20295        cookie.setPath(path_str);
 20296    }
 20297    store->deleteCookie(cookie);
 20298}
 299
 20300void qt_cookies_clear_all(std::uint64_t webview_ptr) {
 20301    QWebEngineView* view = view_of(webview_ptr);
 20302    QWebEngineCookieStore* store = store_of(view);
 20303    if (store == nullptr) {
 0304        return;
 0305    }
 20306    store->deleteAllCookies();
 20307    QWebEngineProfile* profile = view->page()->profile();
 20308    if (profile != nullptr) {
 20309        profile->clearHttpCache();
 20310    }
 311    // NOT cleared (no API for it on a live profile): localStorage /
 312    // sessionStorage, IndexedDB, service-worker registrations and Cache
 313    // Storage. All of them live under the profile's storage directory, which
 314    // this runtime does not delete while the profile is alive. Visited links
 315    // are cleared by `qt_webview_clear_browsing_data()` (the W3 webview op),
 316    // not here: this function is the cookie/HTTP-cache half.
 20317}