bunite-core 0.11.0 → 0.11.2

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.
@@ -0,0 +1,246 @@
1
+ #include "webview2_internal.h"
2
+
3
+ #include <shlobj.h>
4
+
5
+ namespace bunite_webview2 {
6
+
7
+ std::wstring utf8ToWide(const std::string& s) {
8
+ if (s.empty()) return {};
9
+ int n = MultiByteToWideChar(CP_UTF8, 0, s.data(), static_cast<int>(s.size()), nullptr, 0);
10
+ std::wstring out(n, L'\0');
11
+ MultiByteToWideChar(CP_UTF8, 0, s.data(), static_cast<int>(s.size()), out.data(), n);
12
+ return out;
13
+ }
14
+
15
+ std::string wideToUtf8(LPCWSTR s) {
16
+ if (!s || !*s) return {};
17
+ int len = static_cast<int>(wcslen(s));
18
+ int n = WideCharToMultiByte(CP_UTF8, 0, s, len, nullptr, 0, nullptr, nullptr);
19
+ std::string out(n, '\0');
20
+ WideCharToMultiByte(CP_UTF8, 0, s, len, out.data(), n, nullptr, nullptr);
21
+ return out;
22
+ }
23
+
24
+ std::string wideToUtf8(const std::wstring& s) {
25
+ return wideToUtf8(s.c_str());
26
+ }
27
+
28
+ std::string escapeJsonString(const std::string& s) {
29
+ std::string out;
30
+ out.reserve(s.size() + 2);
31
+ for (char c : s) {
32
+ switch (c) {
33
+ case '"': out += "\\\""; break;
34
+ case '\\': out += "\\\\"; break;
35
+ case '\n': out += "\\n"; break;
36
+ case '\r': out += "\\r"; break;
37
+ case '\t': out += "\\t"; break;
38
+ case '\b': out += "\\b"; break;
39
+ case '\f': out += "\\f"; break;
40
+ default:
41
+ if (static_cast<unsigned char>(c) < 0x20) {
42
+ char buf[8];
43
+ snprintf(buf, sizeof(buf), "\\u%04x", c);
44
+ out += buf;
45
+ } else {
46
+ out += c;
47
+ }
48
+ }
49
+ }
50
+ return out;
51
+ }
52
+
53
+ bool globMatchCaseInsensitive(const std::string& pattern, const std::string& value) {
54
+ auto lower = [](char c) -> char {
55
+ return (c >= 'A' && c <= 'Z') ? static_cast<char>(c + 32) : c;
56
+ };
57
+ size_t pi = 0, vi = 0, star = std::string::npos, match = 0;
58
+ while (vi < value.size()) {
59
+ if (pi < pattern.size() && (pattern[pi] == '?' ||
60
+ lower(pattern[pi]) == lower(value[vi]))) {
61
+ pi++; vi++;
62
+ } else if (pi < pattern.size() && pattern[pi] == '*') {
63
+ star = pi++; match = vi;
64
+ } else if (star != std::string::npos) {
65
+ pi = star + 1; vi = ++match;
66
+ } else {
67
+ return false;
68
+ }
69
+ }
70
+ while (pi < pattern.size() && pattern[pi] == '*') pi++;
71
+ return pi == pattern.size();
72
+ }
73
+
74
+ // Very small JSON-array-of-string parser. Tolerant: returns empty vector on
75
+ // any malformed input.
76
+ static std::vector<std::string> parseStringArrayJson(const std::string& json) {
77
+ std::vector<std::string> out;
78
+ size_t i = 0;
79
+ while (i < json.size() && std::isspace(static_cast<unsigned char>(json[i]))) ++i;
80
+ if (i >= json.size() || json[i] != '[') return out;
81
+ ++i;
82
+ while (i < json.size()) {
83
+ while (i < json.size() &&
84
+ (std::isspace(static_cast<unsigned char>(json[i])) || json[i] == ',')) ++i;
85
+ if (i < json.size() && json[i] == ']') break;
86
+ if (i >= json.size() || json[i] != '"') return {};
87
+ ++i;
88
+ std::string item;
89
+ while (i < json.size() && json[i] != '"') {
90
+ if (json[i] == '\\' && i + 1 < json.size()) {
91
+ char nxt = json[i + 1];
92
+ switch (nxt) {
93
+ case 'n': item += '\n'; break;
94
+ case 'r': item += '\r'; break;
95
+ case 't': item += '\t'; break;
96
+ case '"': item += '"'; break;
97
+ case '\\': item += '\\'; break;
98
+ case '/': item += '/'; break;
99
+ default: item += nxt; break;
100
+ }
101
+ i += 2;
102
+ } else {
103
+ item += json[i++];
104
+ }
105
+ }
106
+ if (i < json.size()) ++i; // consume closing quote
107
+ out.push_back(std::move(item));
108
+ }
109
+ return out;
110
+ }
111
+
112
+ std::vector<std::string> parseNavigationRulesJson(const std::string& json) {
113
+ return parseStringArrayJson(json);
114
+ }
115
+
116
+ std::vector<std::string> parsePreloadOriginsJson(const std::string& json) {
117
+ return parseStringArrayJson(json);
118
+ }
119
+
120
+ // Tiny string-keyed JSON object parser for the three keys we care about. We
121
+ // don't want a JSON library dependency for so few values.
122
+ static std::string findStringField(const std::string& json, const std::string& key) {
123
+ std::string needle = "\"" + key + "\"";
124
+ size_t k = json.find(needle);
125
+ if (k == std::string::npos) return {};
126
+ k = json.find(':', k + needle.size());
127
+ if (k == std::string::npos) return {};
128
+ ++k;
129
+ while (k < json.size() && std::isspace(static_cast<unsigned char>(json[k]))) ++k;
130
+ if (k >= json.size() || json[k] != '"') return {};
131
+ ++k;
132
+ std::string out;
133
+ while (k < json.size() && json[k] != '"') {
134
+ if (json[k] == '\\' && k + 1 < json.size()) {
135
+ char nxt = json[k + 1];
136
+ switch (nxt) {
137
+ case 'n': out += '\n'; break;
138
+ case 'r': out += '\r'; break;
139
+ case 't': out += '\t'; break;
140
+ case '"': out += '"'; break;
141
+ case '\\': out += '\\'; break;
142
+ case '/': out += '/'; break;
143
+ default: out += nxt; break;
144
+ }
145
+ k += 2;
146
+ } else {
147
+ out += json[k++];
148
+ }
149
+ }
150
+ return out;
151
+ }
152
+
153
+ void parseEngineConfig(const std::string& json, std::wstring& user_data,
154
+ std::wstring& browser_args, std::wstring& language) {
155
+ if (json.empty()) return;
156
+ std::string s = findStringField(json, "userDataFolder");
157
+ if (!s.empty()) user_data = utf8ToWide(s);
158
+ s = findStringField(json, "additionalBrowserArguments");
159
+ if (!s.empty()) browser_args = utf8ToWide(s);
160
+ s = findStringField(json, "language");
161
+ if (!s.empty()) language = utf8ToWide(s);
162
+ }
163
+
164
+ std::string normalizeAppResPath(const std::string& url) {
165
+ static const std::string prefix = "appres://app.internal";
166
+ if (url.compare(0, prefix.size(), prefix) != 0) return {};
167
+ std::string p = url.substr(prefix.size());
168
+ size_t q = p.find_first_of("?#");
169
+ if (q != std::string::npos) p = p.substr(0, q);
170
+ if (p.empty()) return "/";
171
+ return p;
172
+ }
173
+
174
+ std::string getMimeType(const std::filesystem::path& p) {
175
+ std::string ext = p.extension().string();
176
+ for (auto& c : ext) c = static_cast<char>(::tolower(static_cast<unsigned char>(c)));
177
+ if (ext == ".html" || ext == ".htm") return "text/html; charset=utf-8";
178
+ if (ext == ".js" || ext == ".mjs") return "application/javascript; charset=utf-8";
179
+ if (ext == ".css") return "text/css; charset=utf-8";
180
+ if (ext == ".json") return "application/json; charset=utf-8";
181
+ if (ext == ".svg") return "image/svg+xml";
182
+ if (ext == ".png") return "image/png";
183
+ if (ext == ".jpg" || ext == ".jpeg") return "image/jpeg";
184
+ if (ext == ".gif") return "image/gif";
185
+ if (ext == ".webp") return "image/webp";
186
+ if (ext == ".woff") return "font/woff";
187
+ if (ext == ".woff2") return "font/woff2";
188
+ if (ext == ".ico") return "image/x-icon";
189
+ if (ext == ".wasm") return "application/wasm";
190
+ if (ext == ".map") return "application/json; charset=utf-8";
191
+ return "application/octet-stream";
192
+ }
193
+
194
+ std::wstring exeDir() {
195
+ wchar_t buf[MAX_PATH];
196
+ DWORD n = GetModuleFileNameW(nullptr, buf, MAX_PATH);
197
+ if (n == 0 || n == MAX_PATH) return {};
198
+ std::wstring path(buf, n);
199
+ size_t slash = path.find_last_of(L"\\/");
200
+ return slash == std::wstring::npos ? std::wstring{} : path.substr(0, slash);
201
+ }
202
+
203
+ std::string defaultUserDataFolder() {
204
+ wchar_t* base = nullptr;
205
+ if (SHGetKnownFolderPath(FOLDERID_LocalAppData, 0, nullptr, &base) != S_OK || !base) {
206
+ if (base) CoTaskMemFree(base);
207
+ return {};
208
+ }
209
+ std::wstring full(base);
210
+ CoTaskMemFree(base);
211
+ full += L"\\Bunite\\WebView2";
212
+ return wideToUtf8(full);
213
+ }
214
+
215
+ uint32_t permissionKindToBuniteBit(COREWEBVIEW2_PERMISSION_KIND kind) {
216
+ switch (kind) {
217
+ case COREWEBVIEW2_PERMISSION_KIND_MICROPHONE: return BUNITE_PERMISSION_MICROPHONE;
218
+ case COREWEBVIEW2_PERMISSION_KIND_CAMERA: return BUNITE_PERMISSION_CAMERA;
219
+ case COREWEBVIEW2_PERMISSION_KIND_GEOLOCATION: return BUNITE_PERMISSION_GEOLOCATION;
220
+ case COREWEBVIEW2_PERMISSION_KIND_NOTIFICATIONS: return BUNITE_PERMISSION_NOTIFICATIONS;
221
+ case COREWEBVIEW2_PERMISSION_KIND_CLIPBOARD_READ: return BUNITE_PERMISSION_CLIPBOARD;
222
+ default: return 0;
223
+ }
224
+ }
225
+
226
+ COREWEBVIEW2_PERMISSION_STATE buniteStateToWebView2(uint32_t state) {
227
+ // bunite passes 0=default, 1=allow, 2=deny
228
+ switch (state) {
229
+ case 1: return COREWEBVIEW2_PERMISSION_STATE_ALLOW;
230
+ case 2: return COREWEBVIEW2_PERMISSION_STATE_DENY;
231
+ default: return COREWEBVIEW2_PERMISSION_STATE_DEFAULT;
232
+ }
233
+ }
234
+
235
+ bool shouldAllowNavigation(const ViewHost* view, const std::string& url) {
236
+ if (!view || view->navigation_rules.empty()) return true;
237
+ for (const auto& rule : view->navigation_rules) {
238
+ if (rule.empty()) continue;
239
+ bool allow = (rule[0] != '!');
240
+ const std::string& pat = allow ? rule : rule.substr(1);
241
+ if (globMatchCaseInsensitive(pat, url)) return allow;
242
+ }
243
+ return true;
244
+ }
245
+
246
+ } // namespace bunite_webview2
@@ -10,6 +10,15 @@ import {
10
10
  type WebSocketLike,
11
11
  } from "./index";
12
12
 
13
+ /** Host-provided web globals — set by the page that owns the main Connection so
14
+ * extension bundles (each carrying its own bunite-core copy) share one ws conn
15
+ * instead of opening their own. Desktop has the same property via the CEF
16
+ * preload-injected `window.host.*`; this is the web equivalent. */
17
+ export interface BuniteWebGlobal {
18
+ /** Shared Connection. ensureWebConnection() prefers this over opening a new ws. */
19
+ webConnection?: Connection;
20
+ }
21
+
13
22
  declare global {
14
23
  interface Window {
15
24
  host?: {
@@ -20,6 +29,7 @@ declare global {
20
29
  /** Full Connection for renderer-as-server (serve / serveAll / unserve / replace / on). */
21
30
  getConnection(): Promise<Connection>;
22
31
  };
32
+ __bunite?: BuniteWebGlobal;
23
33
  }
24
34
  }
25
35
 
@@ -35,6 +45,16 @@ function isNative(): boolean {
35
45
 
36
46
  function ensureWebConnection(path = "/rpc"): Promise<Connection> {
37
47
  if (_webConn) return Promise.resolve(_webConn);
48
+ // Host-provided shared Connection — cross-bundle reachability for renderer
49
+ // ecosystems (e.g. extension hosts) that bundle bunite-core 0-externals per
50
+ // plugin. Page-author trust applies; bunite policy/attestation still gates.
51
+ if (typeof window !== "undefined") {
52
+ const shared = window.__bunite?.webConnection;
53
+ if (shared && !shared.closed) {
54
+ _webConn = shared;
55
+ return Promise.resolve(shared);
56
+ }
57
+ }
38
58
  if (_webConnPromise) return _webConnPromise;
39
59
  const attempt = (async () => {
40
60
  const proto = location.protocol === "https:" ? "wss:" : "ws:";