@zogien/native-sdk 0.11.0-fork.3 → 0.11.0-fork.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/build/app.zig +3 -0
- package/build/cli.zig +22 -0
- package/package.json +10 -10
- package/packages/core/package-lock.json +2 -2
- package/packages/core/package.json +1 -1
- package/schemas/app.schema.json +9 -1
- package/skill-data/ts-core/SKILL.md +2 -0
- package/src/app_runner/app_assets.zig +85 -1
- package/src/app_runner/root.zig +63 -3
- package/src/app_runner/ts_core_main.zig +8 -0
- package/src/platform/macos/appkit_host.h +9 -0
- package/src/platform/macos/appkit_host.m +366 -0
- package/src/platform/macos/root.zig +29 -6
- package/src/platform/root.zig +1 -0
- package/src/platform/types.zig +9 -0
- package/src/platform/windows/root.zig +37 -0
- package/src/platform/windows/webview2_host.cpp +371 -26
- package/src/primitives/app_manifest/root.zig +1 -0
- package/src/primitives/app_manifest/tests.zig +35 -0
- package/src/primitives/app_manifest/types.zig +12 -0
- package/src/primitives/app_manifest/validation.zig +44 -6
- package/src/root.zig +1 -0
- package/src/runtime/channel_sources.zig +49 -1
- package/src/runtime/effects.zig +12 -1
- package/src/runtime/flow.zig +6 -0
- package/src/runtime/flow_reference.zig +6 -0
- package/src/runtime/root.zig +4 -0
- package/src/runtime/single_instance.zig +893 -0
- package/src/tooling/manifest.zig +111 -3
- package/src/tooling/package.zig +98 -8
- package/src/tooling/raw_manifest.zig +7 -0
- package/src/tooling/root.zig +2 -0
- package/src/tooling/templates.zig +44 -2
- package/src/tooling/update.zig +228 -10
- package/src/tooling/windows_installer.zig +245 -0
- package/src/tooling/zip_writer.zig +211 -0
- package/src/updater/engine.zig +936 -0
- package/src/updater/feed.zig +261 -12
- package/src/updater/macos.zig +187 -0
- package/src/updater/root.zig +41 -0
- package/src/updater/windows.zig +470 -0
- package/src/windows_install/config.zig +247 -0
- package/src/windows_install/main.zig +690 -0
- package/src/windows_install/payload.zig +146 -0
- package/src/windows_install/switch.zig +184 -0
- package/src/windows_install/tests.zig +11 -0
- package/src/windows_install/win32.zig +248 -0
- package/tools/native-sdk/main.zig +10 -9
|
@@ -421,6 +421,9 @@ struct Window {
|
|
|
421
421
|
/* The maximized state last reported to the runtime, so WM_SIZE emits
|
|
422
422
|
* a frame event only when it changes. */
|
|
423
423
|
bool maximized = false;
|
|
424
|
+
/* Same-thread child kept above content children across the top
|
|
425
|
+
* resize band (see syncTopResizeStrip); created on first need. */
|
|
426
|
+
HWND top_resize_strip = nullptr;
|
|
424
427
|
};
|
|
425
428
|
|
|
426
429
|
/* One rectangle of a canvas view's window-drag mirror (runtime push,
|
|
@@ -743,6 +746,11 @@ struct Host {
|
|
|
743
746
|
std::string window_title;
|
|
744
747
|
std::string bundle_id;
|
|
745
748
|
std::string icon_path;
|
|
749
|
+
/* The WebView2 user data folder every environment in this host
|
|
750
|
+
* shares, resolved once on first use (webViewUserDataFolder). Empty
|
|
751
|
+
* means WebView2's own default. */
|
|
752
|
+
bool webview_user_data_folder_resolved = false;
|
|
753
|
+
std::wstring webview_user_data_folder;
|
|
746
754
|
EventCallback callback = nullptr;
|
|
747
755
|
void *callback_context = nullptr;
|
|
748
756
|
BridgeCallback bridge_callback = nullptr;
|
|
@@ -1171,8 +1179,57 @@ static std::string bridgeOriginForWebViewUrl(const ChildWebView &webview, const
|
|
|
1171
1179
|
return isInternalAssetUrl(webview, url) ? assetOrigin(webview) : originForUrl(url);
|
|
1172
1180
|
}
|
|
1173
1181
|
|
|
1182
|
+
/* The resources\ directory of a packaged Windows artifact: `native
|
|
1183
|
+
* package` stages the executable in bin\ and the app-relative asset tree
|
|
1184
|
+
* in resources\ beside it. Empty when the executable is not in a bin\
|
|
1185
|
+
* directory. */
|
|
1186
|
+
static const std::wstring &packagedResourcesDirectory() {
|
|
1187
|
+
static const std::wstring directory = [] {
|
|
1188
|
+
std::wstring path(MAX_PATH, L'\0');
|
|
1189
|
+
for (;;) {
|
|
1190
|
+
const DWORD length = GetModuleFileNameW(nullptr, path.data(), (DWORD)path.size());
|
|
1191
|
+
if (length == 0) return std::wstring();
|
|
1192
|
+
if (length < path.size()) {
|
|
1193
|
+
path.resize(length);
|
|
1194
|
+
break;
|
|
1195
|
+
}
|
|
1196
|
+
if (path.size() >= 32768) return std::wstring();
|
|
1197
|
+
path.resize(path.size() * 2);
|
|
1198
|
+
}
|
|
1199
|
+
const size_t exe_slash = path.find_last_of(L"\\/");
|
|
1200
|
+
if (exe_slash == std::wstring::npos) return std::wstring();
|
|
1201
|
+
const std::wstring bin = path.substr(0, exe_slash);
|
|
1202
|
+
const size_t bin_slash = bin.find_last_of(L"\\/");
|
|
1203
|
+
if (bin_slash == std::wstring::npos || CompareStringOrdinal(bin.c_str() + bin_slash + 1, -1, L"bin", -1, TRUE) != CSTR_EQUAL) return std::wstring();
|
|
1204
|
+
return bin.substr(0, bin_slash) + L"\\resources";
|
|
1205
|
+
}();
|
|
1206
|
+
return directory;
|
|
1207
|
+
}
|
|
1208
|
+
|
|
1209
|
+
/* A relative WebView asset root names a directory of the packaged asset
|
|
1210
|
+
* tree when the app runs from a package, the tree the macOS host finds
|
|
1211
|
+
* in its bundle's Resources. In development it stays relative to the
|
|
1212
|
+
* working directory. */
|
|
1213
|
+
static std::string assetRootPath(const std::string &root) {
|
|
1214
|
+
const bool absolute = !root.empty() && (root[0] == '\\' || root[0] == '/' || (root.size() > 1 && root[1] == ':'));
|
|
1215
|
+
if (!absolute) {
|
|
1216
|
+
const std::wstring &resources = packagedResourcesDirectory();
|
|
1217
|
+
if (!resources.empty()) {
|
|
1218
|
+
std::wstring candidate = resources;
|
|
1219
|
+
if (!root.empty() && root != ".") {
|
|
1220
|
+
std::wstring relative = widen(root);
|
|
1221
|
+
std::replace(relative.begin(), relative.end(), L'/', L'\\');
|
|
1222
|
+
candidate += L"\\" + relative;
|
|
1223
|
+
}
|
|
1224
|
+
const DWORD attributes = GetFileAttributesW(candidate.c_str());
|
|
1225
|
+
if (attributes != INVALID_FILE_ATTRIBUTES && (attributes & FILE_ATTRIBUTE_DIRECTORY) != 0) return narrow(candidate);
|
|
1226
|
+
}
|
|
1227
|
+
}
|
|
1228
|
+
return root.empty() ? std::string(".") : root;
|
|
1229
|
+
}
|
|
1230
|
+
|
|
1174
1231
|
static std::wstring assetFilePath(const ChildWebView &webview, const std::string &relative) {
|
|
1175
|
-
std::string path = webview.asset_root
|
|
1232
|
+
std::string path = assetRootPath(webview.asset_root);
|
|
1176
1233
|
if (!path.empty() && path.back() != '/' && path.back() != '\\') path.push_back('\\');
|
|
1177
1234
|
std::string native_relative = relative;
|
|
1178
1235
|
std::replace(native_relative.begin(), native_relative.end(), '/', '\\');
|
|
@@ -2014,6 +2071,8 @@ static void applyNativeViewState(NativeView &view, bool update_text, const std::
|
|
|
2014
2071
|
applyNativeViewAccessibility(view);
|
|
2015
2072
|
}
|
|
2016
2073
|
|
|
2074
|
+
static void syncTopResizeStrip(Host *host, uint64_t window_id);
|
|
2075
|
+
|
|
2017
2076
|
static void reorderWindowChildren(Host *host, uint64_t window_id) {
|
|
2018
2077
|
if (!host) return;
|
|
2019
2078
|
struct LayerItem {
|
|
@@ -2041,6 +2100,8 @@ static void reorderWindowChildren(Host *host, uint64_t window_id) {
|
|
|
2041
2100
|
for (const LayerItem &item : items) {
|
|
2042
2101
|
SetWindowPos(item.hwnd, HWND_TOP, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE);
|
|
2043
2102
|
}
|
|
2103
|
+
/* Content layers never rise above the window's own resize band. */
|
|
2104
|
+
syncTopResizeStrip(host, window_id);
|
|
2044
2105
|
}
|
|
2045
2106
|
|
|
2046
2107
|
static bool emitNativeCommandForHwnd(Host *host, HWND hwnd, UINT notification_code) {
|
|
@@ -2328,6 +2389,25 @@ static bool windowIsChromeless(const Window &window) {
|
|
|
2328
2389
|
return window.titlebar_style == 3;
|
|
2329
2390
|
}
|
|
2330
2391
|
|
|
2392
|
+
/* Windows whose top resize frame lives INSIDE the client, answered by
|
|
2393
|
+
* the window's own WM_NCHITTEST and the top resize strip: the hidden
|
|
2394
|
+
* styles, and chromeless windows. On Windows 10 a caption-less frame
|
|
2395
|
+
* with any non-client top draws it as a blank white bar, so chromeless
|
|
2396
|
+
* keeps real left/right/bottom borders (resize and shadow) and hands
|
|
2397
|
+
* the client the top edge, the frame Tauri gives its undecorated
|
|
2398
|
+
* windows. Two chromeless shapes keep the plain popup frame: transparent
|
|
2399
|
+
* windows, whose per-pixel present seeds hit-testable resize bands
|
|
2400
|
+
* around the client, and windows carrying a menu bar, which needs the
|
|
2401
|
+
* non-client top. */
|
|
2402
|
+
static bool windowReclaimsTopFrame(const Window &window, bool has_menu) {
|
|
2403
|
+
if (windowUsesHiddenTitlebar(window)) return true;
|
|
2404
|
+
return windowIsChromeless(window) && !window.transparent && !has_menu;
|
|
2405
|
+
}
|
|
2406
|
+
|
|
2407
|
+
static bool windowReclaimsTopFrame(const Window &window) {
|
|
2408
|
+
return windowReclaimsTopFrame(window, window.hwnd && GetMenu(window.hwnd) != nullptr);
|
|
2409
|
+
}
|
|
2410
|
+
|
|
2331
2411
|
static Window *hiddenTitlebarWindowForHwnd(Host *host, HWND hwnd) {
|
|
2332
2412
|
if (!host || !hwnd) return nullptr;
|
|
2333
2413
|
for (auto &entry : host->windows) {
|
|
@@ -2438,6 +2518,168 @@ static int hiddenFrameTopThickness(HWND hwnd) {
|
|
|
2438
2518
|
return systemMetricForDpi(SM_CYSIZEFRAME, dpi) + systemMetricForDpi(SM_CXPADDEDBORDER, dpi);
|
|
2439
2519
|
}
|
|
2440
2520
|
|
|
2521
|
+
/* The top resize band's hit code for a client point: `thickness` rows
|
|
2522
|
+
* tall, with corner slivers as wide as the side borders so the diagonal
|
|
2523
|
+
* grips survive at the very top; HTNOWHERE outside the band. */
|
|
2524
|
+
static constexpr LRESULT topResizeBandHitCode(int x, int y, int client_width, int thickness, int corner) {
|
|
2525
|
+
if (y < 0 || y >= thickness || x < 0 || x >= client_width) return HTNOWHERE;
|
|
2526
|
+
if (x < corner) return HTTOPLEFT;
|
|
2527
|
+
if (x >= client_width - corner) return HTTOPRIGHT;
|
|
2528
|
+
return HTTOP;
|
|
2529
|
+
}
|
|
2530
|
+
|
|
2531
|
+
static_assert(
|
|
2532
|
+
topResizeBandHitCode(3, 2, 1040, 8, 8) == HTTOPLEFT &&
|
|
2533
|
+
topResizeBandHitCode(8, 2, 1040, 8, 8) == HTTOP &&
|
|
2534
|
+
topResizeBandHitCode(1031, 7, 1040, 8, 8) == HTTOP &&
|
|
2535
|
+
topResizeBandHitCode(1032, 7, 1040, 8, 8) == HTTOPRIGHT &&
|
|
2536
|
+
topResizeBandHitCode(520, 8, 1040, 8, 8) == HTNOWHERE &&
|
|
2537
|
+
topResizeBandHitCode(520, -1, 1040, 8, 8) == HTNOWHERE,
|
|
2538
|
+
"the top band is exactly `thickness` rows tall with `corner`-wide diagonal grips");
|
|
2539
|
+
|
|
2540
|
+
/* The top resize band of a window that reclaims its top frame, in the
|
|
2541
|
+
* window's client coordinates. Only a restored resizable window has one:
|
|
2542
|
+
* a maximized window does not resize and a fixed one never does. Every
|
|
2543
|
+
* surface that answers for the band — the window itself, canvas children
|
|
2544
|
+
* handing it back, and the top resize strip — asks here. */
|
|
2545
|
+
static LRESULT topResizeBandHit(const Window &window, POINT client) {
|
|
2546
|
+
if (!window.hwnd || !windowReclaimsTopFrame(window)) return HTNOWHERE;
|
|
2547
|
+
if (!window.resizable || IsZoomed(window.hwnd) || IsIconic(window.hwnd)) return HTNOWHERE;
|
|
2548
|
+
RECT rect = {};
|
|
2549
|
+
GetClientRect(window.hwnd, &rect);
|
|
2550
|
+
const UINT dpi = dpiForWindow(window.hwnd);
|
|
2551
|
+
const int corner = systemMetricForDpi(SM_CXSIZEFRAME, dpi) + systemMetricForDpi(SM_CXPADDEDBORDER, dpi);
|
|
2552
|
+
return topResizeBandHitCode(client.x, client.y, rect.right - rect.left, hiddenFrameTopThickness(window.hwnd), corner);
|
|
2553
|
+
}
|
|
2554
|
+
|
|
2555
|
+
/* ------------------------------------------------------------------------
|
|
2556
|
+
* Top resize strip.
|
|
2557
|
+
*
|
|
2558
|
+
* A child covering the top band must hand it back to the window. Canvas
|
|
2559
|
+
* surfaces in hidden-titlebar windows do (they answer HTTRANSPARENT
|
|
2560
|
+
* there), but a WebView2 child belongs to the browser process and never
|
|
2561
|
+
* consults its parent, so under WebView content the band was only
|
|
2562
|
+
* resizable where WebView2's own drag-region windows happened to answer
|
|
2563
|
+
* it. The strip is a same-thread, unpainted child kept above every
|
|
2564
|
+
* content child across the band: it answers the window's band hit codes
|
|
2565
|
+
* itself (the cursor follows from them) and forwards the press to the
|
|
2566
|
+
* window, whose DefWindowProc runs the system size loop. Tauri recovers
|
|
2567
|
+
* the top edge of its undecorated windows the same way. WS_EX_TRANSPARENT
|
|
2568
|
+
* keeps the strip out of its siblings' visible regions: the WebView
|
|
2569
|
+
* containers clip siblings, and without it the band shows the window
|
|
2570
|
+
* background instead of the content under it. */
|
|
2571
|
+
|
|
2572
|
+
static Window *windowForTopResizeStrip(Host *host, HWND strip) {
|
|
2573
|
+
if (!host || !strip) return nullptr;
|
|
2574
|
+
for (auto &entry : host->windows) {
|
|
2575
|
+
if (entry.second.top_resize_strip == strip) return &entry.second;
|
|
2576
|
+
}
|
|
2577
|
+
return nullptr;
|
|
2578
|
+
}
|
|
2579
|
+
|
|
2580
|
+
static LRESULT CALLBACK topResizeStripProc(HWND strip, UINT message, WPARAM wparam, LPARAM lparam) {
|
|
2581
|
+
Host *host = reinterpret_cast<Host *>(GetWindowLongPtrW(strip, GWLP_USERDATA));
|
|
2582
|
+
switch (message) {
|
|
2583
|
+
case WM_NCHITTEST: {
|
|
2584
|
+
Window *window = windowForTopResizeStrip(host, strip);
|
|
2585
|
+
if (!window || !window->hwnd) return HTTRANSPARENT;
|
|
2586
|
+
POINT point = { (int)(short)LOWORD(lparam), (int)(short)HIWORD(lparam) };
|
|
2587
|
+
ScreenToClient(window->hwnd, &point);
|
|
2588
|
+
const LRESULT hit = topResizeBandHit(*window, point);
|
|
2589
|
+
return hit != HTNOWHERE ? hit : HTTRANSPARENT;
|
|
2590
|
+
}
|
|
2591
|
+
case WM_NCLBUTTONDOWN:
|
|
2592
|
+
case WM_NCLBUTTONDBLCLK:
|
|
2593
|
+
/* Same thread, so the window's own non-client handling runs
|
|
2594
|
+
* synchronously with the original screen point: the size loop
|
|
2595
|
+
* for a press, the vertical maximize for a double-click. */
|
|
2596
|
+
if (wparam == HTTOP || wparam == HTTOPLEFT || wparam == HTTOPRIGHT) {
|
|
2597
|
+
Window *window = windowForTopResizeStrip(host, strip);
|
|
2598
|
+
if (window && window->hwnd) return SendMessageW(window->hwnd, message, wparam, lparam);
|
|
2599
|
+
}
|
|
2600
|
+
break;
|
|
2601
|
+
case WM_ERASEBKGND:
|
|
2602
|
+
return 1;
|
|
2603
|
+
case WM_PAINT: {
|
|
2604
|
+
PAINTSTRUCT paint = {};
|
|
2605
|
+
BeginPaint(strip, &paint);
|
|
2606
|
+
EndPaint(strip, &paint);
|
|
2607
|
+
return 0;
|
|
2608
|
+
}
|
|
2609
|
+
default:
|
|
2610
|
+
break;
|
|
2611
|
+
}
|
|
2612
|
+
return DefWindowProcW(strip, message, wparam, lparam);
|
|
2613
|
+
}
|
|
2614
|
+
|
|
2615
|
+
static const wchar_t *topResizeStripClassName(Host *host) {
|
|
2616
|
+
static bool registered = false;
|
|
2617
|
+
if (!registered) {
|
|
2618
|
+
WNDCLASSEXW wc = {};
|
|
2619
|
+
wc.cbSize = sizeof(wc);
|
|
2620
|
+
wc.lpfnWndProc = topResizeStripProc;
|
|
2621
|
+
wc.hInstance = host->instance;
|
|
2622
|
+
wc.hCursor = LoadCursor(nullptr, IDC_ARROW);
|
|
2623
|
+
wc.lpszClassName = L"NativeSdkTopResizeStrip";
|
|
2624
|
+
registered = RegisterClassExW(&wc) != 0;
|
|
2625
|
+
}
|
|
2626
|
+
return L"NativeSdkTopResizeStrip";
|
|
2627
|
+
}
|
|
2628
|
+
|
|
2629
|
+
/* Whether any WebView container reaches into the window's top band. */
|
|
2630
|
+
static bool webViewCoversTopBand(Host *host, const Window &window, int band) {
|
|
2631
|
+
RECT client = {};
|
|
2632
|
+
GetClientRect(window.hwnd, &client);
|
|
2633
|
+
const RECT band_rect = { 0, 0, client.right - client.left, band };
|
|
2634
|
+
for (auto &entry : host->webviews) {
|
|
2635
|
+
const ChildWebView &webview = entry.second;
|
|
2636
|
+
if (webview.window_id != window.id || !webview.hwnd) continue;
|
|
2637
|
+
if (!(GetWindowLongPtrW(webview.hwnd, GWL_STYLE) & WS_VISIBLE)) continue;
|
|
2638
|
+
RECT bounds = {};
|
|
2639
|
+
GetWindowRect(webview.hwnd, &bounds);
|
|
2640
|
+
POINT top_left = { bounds.left, bounds.top };
|
|
2641
|
+
POINT bottom_right = { bounds.right, bounds.bottom };
|
|
2642
|
+
ScreenToClient(window.hwnd, &top_left);
|
|
2643
|
+
ScreenToClient(window.hwnd, &bottom_right);
|
|
2644
|
+
const RECT local = { top_left.x, top_left.y, bottom_right.x, bottom_right.y };
|
|
2645
|
+
RECT overlap = {};
|
|
2646
|
+
if (IntersectRect(&overlap, &local, &band_rect)) return true;
|
|
2647
|
+
}
|
|
2648
|
+
return false;
|
|
2649
|
+
}
|
|
2650
|
+
|
|
2651
|
+
/* Show, size and raise the strip over the band of a restored resizable
|
|
2652
|
+
* window that reclaims its top frame; hide it otherwise. Chromeless
|
|
2653
|
+
* windows always take it: their content children answer the band as
|
|
2654
|
+
* client. Hidden-titlebar windows take it only while a WebView covers the
|
|
2655
|
+
* band, so canvas windows keep their own band and the DWM keeps first
|
|
2656
|
+
* claim on its caption buttons. Runs after every geometry, state, or
|
|
2657
|
+
* child z-order change. */
|
|
2658
|
+
static void syncTopResizeStrip(Host *host, uint64_t window_id) {
|
|
2659
|
+
if (!host) return;
|
|
2660
|
+
auto found = host->windows.find(window_id);
|
|
2661
|
+
if (found == host->windows.end() || !found->second.hwnd) return;
|
|
2662
|
+
Window &window = found->second;
|
|
2663
|
+
const int band = hiddenFrameTopThickness(window.hwnd);
|
|
2664
|
+
const bool wanted = windowReclaimsTopFrame(window) && window.resizable && !window.click_through &&
|
|
2665
|
+
!IsZoomed(window.hwnd) && !IsIconic(window.hwnd) &&
|
|
2666
|
+
(windowIsChromeless(window) || webViewCoversTopBand(host, window, band));
|
|
2667
|
+
if (!wanted) {
|
|
2668
|
+
if (window.top_resize_strip) ShowWindow(window.top_resize_strip, SW_HIDE);
|
|
2669
|
+
return;
|
|
2670
|
+
}
|
|
2671
|
+
if (!window.top_resize_strip) {
|
|
2672
|
+
HWND strip = CreateWindowExW(WS_EX_TRANSPARENT, topResizeStripClassName(host), L"", WS_CHILD | WS_CLIPSIBLINGS,
|
|
2673
|
+
0, 0, 0, 0, window.hwnd, nullptr, host->instance, nullptr);
|
|
2674
|
+
if (!strip) return;
|
|
2675
|
+
SetWindowLongPtrW(strip, GWLP_USERDATA, reinterpret_cast<LONG_PTR>(host));
|
|
2676
|
+
window.top_resize_strip = strip;
|
|
2677
|
+
}
|
|
2678
|
+
RECT client = {};
|
|
2679
|
+
GetClientRect(window.hwnd, &client);
|
|
2680
|
+
SetWindowPos(window.top_resize_strip, HWND_TOP, 0, 0, client.right - client.left, band, SWP_NOACTIVATE | SWP_SHOWWINDOW);
|
|
2681
|
+
}
|
|
2682
|
+
|
|
2441
2683
|
/* Height of the standard caption band (frame + caption) at the window's
|
|
2442
2684
|
* dpi — the DWM frame extension depth, and the chrome-channel top inset
|
|
2443
2685
|
* fallback when the live button rects are unavailable. */
|
|
@@ -3448,7 +3690,7 @@ static LRESULT CALLBACK gpuSurfaceProc(HWND hwnd, UINT message, WPARAM wparam, L
|
|
|
3448
3690
|
if (!chrome_window) break;
|
|
3449
3691
|
POINT point = { (int)(short)LOWORD(lparam), (int)(short)HIWORD(lparam) };
|
|
3450
3692
|
ScreenToClient(chrome_window->hwnd, &point);
|
|
3451
|
-
if (
|
|
3693
|
+
if (topResizeBandHit(*chrome_window, point) != HTNOWHERE) return HTTRANSPARENT;
|
|
3452
3694
|
RECT cluster = {};
|
|
3453
3695
|
if (captionButtonsClientRect(chrome_window->hwnd, &cluster) && PtInRect(&cluster, point)) return HTTRANSPARENT;
|
|
3454
3696
|
if (windowDragRegionHit(host, *chrome_window, point)) return HTTRANSPARENT;
|
|
@@ -5010,6 +5252,7 @@ static void destroyAllWindows(Host *host) {
|
|
|
5010
5252
|
if (entry.second.hwnd) {
|
|
5011
5253
|
DestroyWindow(entry.second.hwnd);
|
|
5012
5254
|
entry.second.hwnd = nullptr;
|
|
5255
|
+
entry.second.top_resize_strip = nullptr;
|
|
5013
5256
|
}
|
|
5014
5257
|
}
|
|
5015
5258
|
}
|
|
@@ -5335,13 +5578,90 @@ static CreateEnvironmentFn webView2Factory() {
|
|
|
5335
5578
|
return reinterpret_cast<CreateEnvironmentFn>(GetProcAddress(loader, "CreateCoreWebView2EnvironmentWithOptions"));
|
|
5336
5579
|
}
|
|
5337
5580
|
|
|
5581
|
+
static bool webViewDirectoryExists(const std::wstring &path) {
|
|
5582
|
+
const DWORD attributes = GetFileAttributesW(path.c_str());
|
|
5583
|
+
return attributes != INVALID_FILE_ATTRIBUTES && (attributes & FILE_ATTRIBUTE_DIRECTORY) != 0;
|
|
5584
|
+
}
|
|
5585
|
+
|
|
5586
|
+
/* The folder WebView2 picks when none is given: "<exe>.WebView2" beside
|
|
5587
|
+
* the executable. Earlier hosts left their data there. */
|
|
5588
|
+
static std::wstring webViewLegacyUserDataFolder() {
|
|
5589
|
+
std::wstring path(MAX_PATH, L'\0');
|
|
5590
|
+
for (;;) {
|
|
5591
|
+
const DWORD length = GetModuleFileNameW(nullptr, path.data(), (DWORD)path.size());
|
|
5592
|
+
if (length == 0) return std::wstring();
|
|
5593
|
+
if (length < path.size()) {
|
|
5594
|
+
path.resize(length);
|
|
5595
|
+
return path + L".WebView2";
|
|
5596
|
+
}
|
|
5597
|
+
if (path.size() >= 32768) return std::wstring();
|
|
5598
|
+
path.resize(path.size() * 2);
|
|
5599
|
+
}
|
|
5600
|
+
}
|
|
5601
|
+
|
|
5602
|
+
/* WebView2 keeps cookies, storage and caches in its user data folder.
|
|
5603
|
+
* Its default sits beside the executable, which is read-only under
|
|
5604
|
+
* Program Files and is replaced together with the program files when
|
|
5605
|
+
* an installed app updates. Keep it with the app's other per-user data
|
|
5606
|
+
* instead: %LOCALAPPDATA%\<bundle id>\WebView2, next to the Data, Cache,
|
|
5607
|
+
* State and Logs folders app_dirs resolves from the same variable and
|
|
5608
|
+
* bundle id. A folder left beside the executable by an earlier host
|
|
5609
|
+
* moves there once. A folder on another volume cannot be renamed there,
|
|
5610
|
+
* so that install keeps its old folder; when the move fails for another
|
|
5611
|
+
* reason (another process still has the folder open) this launch keeps
|
|
5612
|
+
* the old folder and the next launch tries again. Either way no data is
|
|
5613
|
+
* lost. Without LOCALAPPDATA or a usable bundle id, WebView2 keeps its
|
|
5614
|
+
* default. */
|
|
5615
|
+
static const wchar_t *webViewUserDataFolder(Host *host) {
|
|
5616
|
+
if (host->webview_user_data_folder_resolved) {
|
|
5617
|
+
return host->webview_user_data_folder.empty() ? nullptr : host->webview_user_data_folder.c_str();
|
|
5618
|
+
}
|
|
5619
|
+
host->webview_user_data_folder_resolved = true;
|
|
5620
|
+
const std::string &bundle_id = host->bundle_id;
|
|
5621
|
+
if (bundle_id.empty() || bundle_id == "." || bundle_id == ".." ||
|
|
5622
|
+
bundle_id.find_first_of("\\/:*?\"<>|") != std::string::npos) return nullptr;
|
|
5623
|
+
for (unsigned char ch : bundle_id) {
|
|
5624
|
+
if (ch < 0x20) return nullptr;
|
|
5625
|
+
}
|
|
5626
|
+
const DWORD needed = GetEnvironmentVariableW(L"LOCALAPPDATA", nullptr, 0);
|
|
5627
|
+
if (needed <= 1) return nullptr;
|
|
5628
|
+
std::wstring local_app_data((size_t)needed, L'\0');
|
|
5629
|
+
const DWORD written = GetEnvironmentVariableW(L"LOCALAPPDATA", local_app_data.data(), needed);
|
|
5630
|
+
if (written == 0 || written >= needed) return nullptr;
|
|
5631
|
+
local_app_data.resize(written);
|
|
5632
|
+
while (!local_app_data.empty() && (local_app_data.back() == L'\\' || local_app_data.back() == L'/')) local_app_data.pop_back();
|
|
5633
|
+
if (local_app_data.empty()) return nullptr;
|
|
5634
|
+
|
|
5635
|
+
const std::wstring app_root = local_app_data + L"\\" + widen(bundle_id);
|
|
5636
|
+
const std::wstring folder = app_root + L"\\WebView2";
|
|
5637
|
+
SHCreateDirectoryExW(nullptr, app_root.c_str(), nullptr);
|
|
5638
|
+
if (!webViewDirectoryExists(app_root)) return nullptr;
|
|
5639
|
+
|
|
5640
|
+
if (!webViewDirectoryExists(folder)) {
|
|
5641
|
+
const std::wstring legacy = webViewLegacyUserDataFolder();
|
|
5642
|
+
if (!legacy.empty() && webViewDirectoryExists(legacy) && !MoveFileExW(legacy.c_str(), folder.c_str(), 0)) {
|
|
5643
|
+
const DWORD error = GetLastError();
|
|
5644
|
+
if (error != ERROR_NOT_SAME_DEVICE) {
|
|
5645
|
+
fprintf(stderr, "native-sdk: keeping WebView2 data in %s for this launch; moving it to %s failed (error %lu)\n",
|
|
5646
|
+
narrow(legacy).c_str(), narrow(folder).c_str(), static_cast<unsigned long>(error));
|
|
5647
|
+
}
|
|
5648
|
+
host->webview_user_data_folder = legacy;
|
|
5649
|
+
return host->webview_user_data_folder.c_str();
|
|
5650
|
+
}
|
|
5651
|
+
}
|
|
5652
|
+
host->webview_user_data_folder = folder;
|
|
5653
|
+
return host->webview_user_data_folder.c_str();
|
|
5654
|
+
}
|
|
5655
|
+
|
|
5338
5656
|
static void cleanupPendingChildWebView(Host *host, const std::string &key) {
|
|
5339
5657
|
if (!host) return;
|
|
5340
5658
|
auto found = host->webviews.find(key);
|
|
5341
5659
|
if (found == host->webviews.end()) return;
|
|
5660
|
+
const uint64_t window_id = found->second.window_id;
|
|
5342
5661
|
if (found->second.controller) found->second.controller->Close();
|
|
5343
5662
|
if (found->second.hwnd) DestroyWindow(found->second.hwnd);
|
|
5344
5663
|
host->webviews.erase(found);
|
|
5664
|
+
syncTopResizeStrip(host, window_id);
|
|
5345
5665
|
}
|
|
5346
5666
|
|
|
5347
5667
|
static void failWindowDragInitialization(Host *host, const std::string &key, HRESULT result) {
|
|
@@ -5361,7 +5681,7 @@ static bool createChildWebView(Host *host, const std::string &key) {
|
|
|
5361
5681
|
HWND parent = found->second.hwnd;
|
|
5362
5682
|
const uint64_t generation = found->second.window_drag_generation;
|
|
5363
5683
|
std::weak_ptr<HostLifetime> lifetime = host->lifetime;
|
|
5364
|
-
HRESULT hr = factory(nullptr,
|
|
5684
|
+
HRESULT hr = factory(nullptr, webViewUserDataFolder(host), nullptr, Callback<ICoreWebView2CreateCoreWebView2EnvironmentCompletedHandler>(
|
|
5365
5685
|
[host, key, parent, lifetime, generation](HRESULT result, ICoreWebView2Environment *environment) -> HRESULT {
|
|
5366
5686
|
auto token = lifetime.lock();
|
|
5367
5687
|
if (!token) return S_OK;
|
|
@@ -5818,21 +6138,11 @@ static LRESULT CALLBACK windowProc(HWND hwnd, UINT message, WPARAM wparam, LPARA
|
|
|
5818
6138
|
if (def_hit != HTCLIENT) return def_hit;
|
|
5819
6139
|
POINT point = { (int)(short)LOWORD(lparam), (int)(short)HIWORD(lparam) };
|
|
5820
6140
|
ScreenToClient(hwnd, &point);
|
|
5821
|
-
RECT client = {};
|
|
5822
|
-
GetClientRect(hwnd, &client);
|
|
5823
6141
|
/* Top resize band: the caption removal moved the top
|
|
5824
6142
|
* frame INSIDE the client area, so hand its band back to
|
|
5825
|
-
* the system
|
|
5826
|
-
|
|
5827
|
-
|
|
5828
|
-
* diagonal grips survive at the very top. */
|
|
5829
|
-
if (chrome_window->resizable && !IsZoomed(hwnd) && point.y >= 0 && point.y < hiddenFrameTopThickness(hwnd)) {
|
|
5830
|
-
const UINT dpi = dpiForWindow(hwnd);
|
|
5831
|
-
const int corner = systemMetricForDpi(SM_CXSIZEFRAME, dpi) + systemMetricForDpi(SM_CXPADDEDBORDER, dpi);
|
|
5832
|
-
if (point.x < corner) return HTTOPLEFT;
|
|
5833
|
-
if (point.x >= client.right - corner) return HTTOPRIGHT;
|
|
5834
|
-
return HTTOP;
|
|
5835
|
-
}
|
|
6143
|
+
* the system. */
|
|
6144
|
+
const LRESULT band_hit = topResizeBandHit(*chrome_window, point);
|
|
6145
|
+
if (band_hit != HTNOWHERE) return band_hit;
|
|
5836
6146
|
/* Caption buttons when DwmDefWindowProc stayed silent
|
|
5837
6147
|
* (composition off, older cores): DefWindowProc still
|
|
5838
6148
|
* performs minimize/maximize/close for these hit codes,
|
|
@@ -5865,6 +6175,22 @@ static LRESULT CALLBACK windowProc(HWND hwnd, UINT message, WPARAM wparam, LPARA
|
|
|
5865
6175
|
break;
|
|
5866
6176
|
}
|
|
5867
6177
|
}
|
|
6178
|
+
/* Chromeless windows hand the top frame to the client (see
|
|
6179
|
+
* windowReclaimsTopFrame) while restored. A maximized window keeps
|
|
6180
|
+
* DefWindowProc's inset: its frame parks the borders offscreen around
|
|
6181
|
+
* the work area, and the inset is what lands the client exactly on
|
|
6182
|
+
* it. */
|
|
6183
|
+
if (message == WM_NCCALCSIZE && wparam) {
|
|
6184
|
+
Window *chromeless_window = chromelessWindowForHwnd(host, hwnd);
|
|
6185
|
+
if (chromeless_window && windowReclaimsTopFrame(*chromeless_window) && !IsZoomed(hwnd)) {
|
|
6186
|
+
NCCALCSIZE_PARAMS *params = reinterpret_cast<NCCALCSIZE_PARAMS *>(lparam);
|
|
6187
|
+
const LONG original_top = params->rgrc[0].top;
|
|
6188
|
+
const LRESULT def_result = DefWindowProcW(hwnd, WM_NCCALCSIZE, wparam, lparam);
|
|
6189
|
+
if (def_result != 0) return def_result;
|
|
6190
|
+
params->rgrc[0].top = original_top;
|
|
6191
|
+
return 0;
|
|
6192
|
+
}
|
|
6193
|
+
}
|
|
5868
6194
|
/* Chromeless windows (WS_POPUP, no caption): DefWindowProc owns the
|
|
5869
6195
|
* resize frame when one exists; the app's window-drag regions are
|
|
5870
6196
|
* the only caption there is, so a client-area hit inside one
|
|
@@ -5877,6 +6203,8 @@ static LRESULT CALLBACK windowProc(HWND hwnd, UINT message, WPARAM wparam, LPARA
|
|
|
5877
6203
|
if (def_hit != HTCLIENT) return def_hit;
|
|
5878
6204
|
POINT point = { (int)(short)LOWORD(lparam), (int)(short)HIWORD(lparam) };
|
|
5879
6205
|
ScreenToClient(hwnd, &point);
|
|
6206
|
+
const LRESULT band_hit = topResizeBandHit(*chromeless_window, point);
|
|
6207
|
+
if (band_hit != HTNOWHERE) return band_hit;
|
|
5880
6208
|
if (windowDragRegionHit(host, *chromeless_window, point)) return HTCAPTION;
|
|
5881
6209
|
return HTCLIENT;
|
|
5882
6210
|
}
|
|
@@ -5990,6 +6318,9 @@ static LRESULT CALLBACK windowProc(HWND hwnd, UINT message, WPARAM wparam, LPARA
|
|
|
5990
6318
|
applyWebViewFrame(main->second);
|
|
5991
6319
|
}
|
|
5992
6320
|
#endif
|
|
6321
|
+
/* Size, maximize/restore, minimize, and dpi changes
|
|
6322
|
+
* all land here. */
|
|
6323
|
+
syncTopResizeStrip(host, entry.first);
|
|
5993
6324
|
emit(host, entry.second, kResize);
|
|
5994
6325
|
}
|
|
5995
6326
|
}
|
|
@@ -6109,9 +6440,9 @@ static LRESULT CALLBACK windowProc(HWND hwnd, UINT message, WPARAM wparam, LPARA
|
|
|
6109
6440
|
/* The declared floor is a CONTENT size in LOGICAL
|
|
6110
6441
|
* points; scale to physical pixels at this window's
|
|
6111
6442
|
* DPI, then convert to the outer track size for its
|
|
6112
|
-
* current style.
|
|
6113
|
-
* chrome (the custom WM_NCCALCSIZE hands
|
|
6114
|
-
*
|
|
6443
|
+
* current style. Styles that reclaim their top frame
|
|
6444
|
+
* carry no top chrome (the custom WM_NCCALCSIZE hands
|
|
6445
|
+
* it to the client), so their conversion skips it
|
|
6115
6446
|
* too. */
|
|
6116
6447
|
const UINT dpi = dpiForWindow(hwnd);
|
|
6117
6448
|
const double scale = (double)dpi / 96.0;
|
|
@@ -6124,7 +6455,7 @@ static LRESULT CALLBACK windowProc(HWND hwnd, UINT message, WPARAM wparam, LPARA
|
|
|
6124
6455
|
adjustWindowRectForDpi(&frame, style, has_menu, ex_style, dpi);
|
|
6125
6456
|
LONG outer_width = frame.right - frame.left;
|
|
6126
6457
|
LONG outer_height = frame.bottom - frame.top;
|
|
6127
|
-
if (
|
|
6458
|
+
if (windowReclaimsTopFrame(window, has_menu)) {
|
|
6128
6459
|
const SIZE outer = hiddenOuterSizeForContent(style, ex_style, has_menu, min_content_width, min_content_height, dpi);
|
|
6129
6460
|
outer_width = outer.cx;
|
|
6130
6461
|
outer_height = outer.cy;
|
|
@@ -6183,6 +6514,7 @@ static LRESULT CALLBACK windowProc(HWND hwnd, UINT message, WPARAM wparam, LPARA
|
|
|
6183
6514
|
destroyNativeViewsForWindow(host, entry.first);
|
|
6184
6515
|
destroyChildWebViewsForWindow(host, entry.first);
|
|
6185
6516
|
entry.second.hwnd = nullptr;
|
|
6517
|
+
entry.second.top_resize_strip = nullptr;
|
|
6186
6518
|
/* Destroyed means closed, never hidden. */
|
|
6187
6519
|
entry.second.policy_hidden = false;
|
|
6188
6520
|
emit(host, entry.second, kWindowFrame);
|
|
@@ -6252,9 +6584,9 @@ static bool createNativeWindow(Host *host, Window &window) {
|
|
|
6252
6584
|
* re-derives the frame if it lands on another monitor), then grow
|
|
6253
6585
|
* it to the outer size for this style so the client rect lands at
|
|
6254
6586
|
* the request. The menu bar is attached after creation, so account
|
|
6255
|
-
* for it here when menus are declared.
|
|
6256
|
-
* custom-calc shape (no top chrome) — plain adjustment
|
|
6257
|
-
* their client one caption band tall. */
|
|
6587
|
+
* for it here when menus are declared. Styles that reclaim their top
|
|
6588
|
+
* frame use the custom-calc shape (no top chrome) — plain adjustment
|
|
6589
|
+
* would land their client one caption band (or top border) tall. */
|
|
6258
6590
|
const bool has_menu = !host->menus.empty();
|
|
6259
6591
|
const UINT dpi = systemDpi();
|
|
6260
6592
|
const double scale = (double)dpi / 96.0;
|
|
@@ -6264,7 +6596,7 @@ static bool createNativeWindow(Host *host, Window &window) {
|
|
|
6264
6596
|
adjustWindowRectForDpi(&frame, style, has_menu ? TRUE : FALSE, 0, dpi);
|
|
6265
6597
|
LONG outer_width = frame.right - frame.left;
|
|
6266
6598
|
LONG outer_height = frame.bottom - frame.top;
|
|
6267
|
-
if (
|
|
6599
|
+
if (windowReclaimsTopFrame(window, has_menu)) {
|
|
6268
6600
|
const SIZE outer = hiddenOuterSizeForContent(style, 0, has_menu, content_width, content_height, dpi);
|
|
6269
6601
|
outer_width = outer.cx;
|
|
6270
6602
|
outer_height = outer.cy;
|
|
@@ -6296,13 +6628,14 @@ static bool createNativeWindow(Host *host, Window &window) {
|
|
|
6296
6628
|
/* Before the first show, so a dark-scheme launch never flashes a
|
|
6297
6629
|
* light caption. */
|
|
6298
6630
|
applyStandardTitlebarColorScheme(window);
|
|
6299
|
-
if (windowUsesHiddenTitlebar(window))
|
|
6300
|
-
|
|
6631
|
+
if (windowUsesHiddenTitlebar(window)) applyHiddenTitlebarFrame(window);
|
|
6632
|
+
if (windowReclaimsTopFrame(window)) {
|
|
6301
6633
|
/* The create-time WM_NCCALCSIZE ran before this window was
|
|
6302
6634
|
* registered under its HWND, so the custom calc did not apply;
|
|
6303
6635
|
* force one now that the map entry resolves. The callers keep
|
|
6304
6636
|
* `window` referencing the stored map entry for exactly this. */
|
|
6305
6637
|
SetWindowPos(hwnd, nullptr, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER | SWP_NOACTIVATE | SWP_FRAMECHANGED);
|
|
6638
|
+
syncTopResizeStrip(host, window.id);
|
|
6306
6639
|
}
|
|
6307
6640
|
/* Initialize WS_EX_LAYERED through the same per-pixel path used by
|
|
6308
6641
|
* canvas presents. A layered HWND is otherwise not displayable until
|
|
@@ -6357,6 +6690,14 @@ Host *native_sdk_windows_create(const char *app_name, size_t app_name_len, const
|
|
|
6357
6690
|
host->app_name = slice(app_name, app_name_len);
|
|
6358
6691
|
host->window_title = slice(window_title, window_title_len);
|
|
6359
6692
|
host->bundle_id = slice(bundle_id, bundle_id_len);
|
|
6693
|
+
/* The taskbar groups and pins windows by AppUserModelID. Name the
|
|
6694
|
+
* process by its bundle id, the id the installer's Start menu
|
|
6695
|
+
* shortcut carries, so the taskbar follows the app instead of the
|
|
6696
|
+
* path of the executable that happened to start it. */
|
|
6697
|
+
if (!host->bundle_id.empty()) {
|
|
6698
|
+
const std::wstring app_user_model_id = widen(host->bundle_id);
|
|
6699
|
+
SetCurrentProcessExplicitAppUserModelID(app_user_model_id.c_str());
|
|
6700
|
+
}
|
|
6360
6701
|
host->icon_path = slice(icon_path, icon_path_len);
|
|
6361
6702
|
Window window;
|
|
6362
6703
|
window.id = 1;
|
|
@@ -6638,6 +6979,7 @@ int native_sdk_windows_load_window_webview(Host *host, uint64_t window_id, const
|
|
|
6638
6979
|
host->webviews.erase(key);
|
|
6639
6980
|
return 0;
|
|
6640
6981
|
}
|
|
6982
|
+
syncTopResizeStrip(host, window_id);
|
|
6641
6983
|
}
|
|
6642
6984
|
|
|
6643
6985
|
ChildWebView &webview = found->second;
|
|
@@ -8042,6 +8384,7 @@ int native_sdk_windows_create_webview(Host *host, uint64_t window_id, const char
|
|
|
8042
8384
|
if (!createChildWebView(host, key)) {
|
|
8043
8385
|
DestroyWindow(hwnd);
|
|
8044
8386
|
host->webviews.erase(key);
|
|
8387
|
+
syncTopResizeStrip(host, window_id);
|
|
8045
8388
|
return 0;
|
|
8046
8389
|
}
|
|
8047
8390
|
return 1;
|
|
@@ -8066,6 +8409,7 @@ int native_sdk_windows_set_webview_frame(Host *host, uint64_t window_id, const c
|
|
|
8066
8409
|
found->second.controller->put_Bounds(bounds);
|
|
8067
8410
|
}
|
|
8068
8411
|
#endif
|
|
8412
|
+
syncTopResizeStrip(host, window_id);
|
|
8069
8413
|
return 1;
|
|
8070
8414
|
}
|
|
8071
8415
|
|
|
@@ -8145,6 +8489,7 @@ int native_sdk_windows_close_webview(Host *host, uint64_t window_id, const char
|
|
|
8145
8489
|
#endif
|
|
8146
8490
|
if (found->second.hwnd) DestroyWindow(found->second.hwnd);
|
|
8147
8491
|
host->webviews.erase(found);
|
|
8492
|
+
syncTopResizeStrip(host, window_id);
|
|
8148
8493
|
return 1;
|
|
8149
8494
|
}
|
|
8150
8495
|
|
|
@@ -83,6 +83,7 @@ pub const FileAssociation = types.FileAssociation;
|
|
|
83
83
|
pub const UrlScheme = types.UrlScheme;
|
|
84
84
|
pub const PackageMetadata = types.PackageMetadata;
|
|
85
85
|
pub const UpdateConfig = types.UpdateConfig;
|
|
86
|
+
pub const UpdateMode = types.UpdateMode;
|
|
86
87
|
pub const Manifest = types.Manifest;
|
|
87
88
|
pub const ImagesConfig = types.ImagesConfig;
|
|
88
89
|
pub const validateManifest = validation.validateManifest;
|
|
@@ -125,6 +125,41 @@ test "updates require paired HTTPS feed and Ed25519 public key" {
|
|
|
125
125
|
}));
|
|
126
126
|
}
|
|
127
127
|
|
|
128
|
+
test "app-driven updates need a feed to drive" {
|
|
129
|
+
try validateUpdates(.{
|
|
130
|
+
.feed_url = "https://example.com/native-update.json",
|
|
131
|
+
.public_key = "11qYAYKxCrfVS/7TyWQHOg7hcvPapiMlrwIaaPcHURo=",
|
|
132
|
+
.mode = .app,
|
|
133
|
+
});
|
|
134
|
+
try std.testing.expectError(error.MissingRequiredField, validateUpdates(.{ .mode = .app }));
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
test "update feed URLs may name the target and installed version" {
|
|
138
|
+
const key = "11qYAYKxCrfVS/7TyWQHOg7hcvPapiMlrwIaaPcHURo=";
|
|
139
|
+
try validateUpdates(.{ .feed_url = "https://example.com/releases/{target}/native-update.json", .public_key = key });
|
|
140
|
+
try validateUpdates(.{ .feed_url = "https://example.com/feed?target={target}&from={current_version}", .public_key = key });
|
|
141
|
+
try std.testing.expectError(error.InvalidUrl, validateUpdates(.{ .feed_url = "https://example.com/{arch}/native-update.json", .public_key = key }));
|
|
142
|
+
try std.testing.expectError(error.InvalidUrl, validateUpdates(.{ .feed_url = "https://example.com/{target/native-update.json", .public_key = key }));
|
|
143
|
+
try std.testing.expectError(error.InvalidUrl, validateUpdates(.{ .feed_url = "https://example.com/target}/native-update.json", .public_key = key }));
|
|
144
|
+
try std.testing.expectError(error.InvalidUrl, validateUpdates(.{ .feed_url = "https://{target}.example.com/native-update.json", .public_key = key }));
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
test "update public_keys add distinct trusted keys beside public_key" {
|
|
148
|
+
const key = "11qYAYKxCrfVS/7TyWQHOg7hcvPapiMlrwIaaPcHURo=";
|
|
149
|
+
const other = "wFDFY3pE+oYp//PMzOIwDLNipj2Z2V/FQUUmb0MyRFo=";
|
|
150
|
+
const rotated = [_][]const u8{other};
|
|
151
|
+
try validateUpdates(.{ .feed_url = "https://example.com/native-update.json", .public_key = key, .public_keys = &rotated });
|
|
152
|
+
try std.testing.expectError(error.MissingRequiredField, validateUpdates(.{ .public_keys = &rotated }));
|
|
153
|
+
const repeated = [_][]const u8{key};
|
|
154
|
+
try std.testing.expectError(error.MissingRequiredField, validateUpdates(.{ .feed_url = "https://example.com/native-update.json", .public_key = key, .public_keys = &repeated }));
|
|
155
|
+
const duplicated = [_][]const u8{ other, other };
|
|
156
|
+
try std.testing.expectError(error.MissingRequiredField, validateUpdates(.{ .feed_url = "https://example.com/native-update.json", .public_key = key, .public_keys = &duplicated }));
|
|
157
|
+
const malformed = [_][]const u8{"not-a-public-key"};
|
|
158
|
+
try std.testing.expectError(error.MissingRequiredField, validateUpdates(.{ .feed_url = "https://example.com/native-update.json", .public_key = key, .public_keys = &malformed }));
|
|
159
|
+
const too_many = [_][]const u8{ other, "IBLLkMpg6OXY2vZuInLSIz4EhtVX6MZhQe2JIBd9frc=", "+AzM3OSuHAeuIIoq35mjEK5CB+Awb6AjYRCwaCe7uNA=", "n0lDnp2wlbxBEe0l01eV2DG8VaBH9LHX9q7jd3u0EiA=" };
|
|
160
|
+
try std.testing.expectError(error.MissingRequiredField, validateUpdates(.{ .feed_url = "https://example.com/native-update.json", .public_key = key, .public_keys = &too_many }));
|
|
161
|
+
}
|
|
162
|
+
|
|
128
163
|
test "accessory startup requires the tray capability" {
|
|
129
164
|
const tray_capabilities = [_]Capability{.tray};
|
|
130
165
|
try validateManifest(.{
|
|
@@ -647,9 +647,21 @@ pub const PackageMetadata = struct {
|
|
|
647
647
|
pub const UpdateConfig = struct {
|
|
648
648
|
feed_url: ?[]const u8 = null,
|
|
649
649
|
public_key: ?[]const u8 = null,
|
|
650
|
+
/// Further trusted keys beside `public_key`, so a release signed with
|
|
651
|
+
/// a key being rotated out still verifies on installs that know it.
|
|
652
|
+
public_keys: []const []const u8 = &.{},
|
|
650
653
|
check_on_start: bool = false,
|
|
654
|
+
/// Who drives updates on macOS: the host's own prompts, or the app
|
|
655
|
+
/// through the `native-sdk.updates` requests and channel. Windows apps
|
|
656
|
+
/// always drive updates themselves.
|
|
657
|
+
mode: UpdateMode = .host,
|
|
651
658
|
};
|
|
652
659
|
|
|
660
|
+
pub const UpdateMode = enum { host, app };
|
|
661
|
+
|
|
662
|
+
/// `public_key` plus at most this many `public_keys`.
|
|
663
|
+
pub const max_update_public_keys: usize = 3;
|
|
664
|
+
|
|
653
665
|
pub const PersistRestore = struct {
|
|
654
666
|
ok: []const u8,
|
|
655
667
|
none: []const u8,
|