@swf2es/desktop 0.1.20 → 0.1.22

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.
Files changed (54) hide show
  1. package/dist/main/air-bridge.d.ts +7 -0
  2. package/dist/main/air-bridge.d.ts.map +1 -0
  3. package/dist/main/air-bridge.js +166 -0
  4. package/dist/main/air-bridge.js.map +1 -0
  5. package/dist/main/dialogs.d.ts +18 -0
  6. package/dist/main/dialogs.d.ts.map +1 -0
  7. package/dist/main/dialogs.js +128 -0
  8. package/dist/main/dialogs.js.map +1 -0
  9. package/dist/main/main.js +28 -788
  10. package/dist/main/main.js.map +1 -1
  11. package/dist/main/menu.d.ts +2 -0
  12. package/dist/main/menu.d.ts.map +1 -0
  13. package/dist/main/menu.js +80 -0
  14. package/dist/main/menu.js.map +1 -0
  15. package/dist/main/network-rules.d.ts +35 -0
  16. package/dist/main/network-rules.d.ts.map +1 -0
  17. package/dist/main/network-rules.js +167 -0
  18. package/dist/main/network-rules.js.map +1 -0
  19. package/dist/main/network-types.d.ts +131 -0
  20. package/dist/main/network-types.d.ts.map +1 -0
  21. package/dist/main/network-types.js +21 -0
  22. package/dist/main/network-types.js.map +1 -0
  23. package/dist/main/network.d.ts +1 -113
  24. package/dist/main/network.d.ts.map +1 -1
  25. package/dist/main/network.js +8 -181
  26. package/dist/main/network.js.map +1 -1
  27. package/dist/main/open.d.ts +55 -0
  28. package/dist/main/open.d.ts.map +1 -0
  29. package/dist/main/open.js +256 -0
  30. package/dist/main/open.js.map +1 -0
  31. package/dist/main/permissions.d.ts +17 -0
  32. package/dist/main/permissions.d.ts.map +1 -0
  33. package/dist/main/permissions.js +113 -0
  34. package/dist/main/permissions.js.map +1 -0
  35. package/dist/main/state.d.ts +17 -0
  36. package/dist/main/state.d.ts.map +1 -0
  37. package/dist/main/state.js +30 -0
  38. package/dist/main/state.js.map +1 -0
  39. package/dist/main/switches.d.ts +25 -0
  40. package/dist/main/switches.d.ts.map +1 -0
  41. package/dist/main/switches.js +69 -0
  42. package/dist/main/switches.js.map +1 -0
  43. package/package.json +7 -7
  44. package/src/main/air-bridge.ts +185 -0
  45. package/src/main/dialogs.ts +147 -0
  46. package/src/main/main.ts +56 -937
  47. package/src/main/menu.ts +84 -0
  48. package/src/main/network-rules.ts +199 -0
  49. package/src/main/network-types.ts +150 -0
  50. package/src/main/network.ts +30 -337
  51. package/src/main/open.ts +302 -0
  52. package/src/main/permissions.ts +135 -0
  53. package/src/main/state.ts +44 -0
  54. package/src/main/switches.ts +87 -0
package/dist/main/main.js CHANGED
@@ -9,30 +9,33 @@
9
9
  // --proxy=socks5://[user:pass@]host:port (or http://...) sends the SWF's network through a proxy.
10
10
  //
11
11
  // The second runs an AIR application by its descriptor, as adl does.
12
- import { statSync } from "node:fs";
13
- import { open as openFile, readFile, realpath } from "node:fs/promises";
14
- import { basename, dirname, isAbsolute, join, resolve } from "node:path";
12
+ //
13
+ // Its parts: state.ts, what they share; switches.ts, the command line's;
14
+ // open.ts, what plays and opening it; permissions.ts, sockets and
15
+ // WebSockets; dialogs.ts and menu.ts; air-bridge.ts, an AIR application's
16
+ // reach. Here: start-up, the window and its page's messages.
17
+ import { isAbsolute } from "node:path";
15
18
  import { fileURLToPath } from "node:url";
16
- import { fileAttributes } from "@swf2es/format";
17
- import { app, BrowserWindow, dialog, ipcMain, Menu, safeStorage, screen, session, shell, } from "electron";
18
- import { airCommand, airLaunch, createDirectoryOf, EncryptedItems, listOf, moveOf, readOf, removeOf, statOf, writeOf, } from "./air.js";
19
- import { resolveCase } from "./case-paths.js";
19
+ import { app, BrowserWindow, dialog, ipcMain, session, shell } from "electron";
20
+ import { airCommand } from "./air.js";
21
+ import { listenForAir } from "./air-bridge.js";
22
+ import { chooseLibrary, fromPrompt, prompt, showOpenDialog, showUrlDialog } from "./dialogs.js";
20
23
  import { bridgeFiles } from "./files.js";
24
+ import { buildMenu } from "./menu.js";
21
25
  import { failureLine, requestLine } from "./netlog.js";
22
- import { Network, Refused, unverifiedHosts } from "./network.js";
26
+ import { Refused } from "./network-types.js";
27
+ import { current, movieFromArguments, networkMovie, openAir, openChosen, openEntry, openUrl, remoteUrl, } from "./open.js";
28
+ import { permitSocket, permitWebSocket } from "./permissions.js";
23
29
  import { APP_ORIGIN, registerScheme, serve } from "./protocol.js";
24
- import { describeProxy, proxyArgument, sameProxy } from "./proxy.js";
25
- import { Sandbox } from "./sandbox.js";
30
+ import { proxyArgument, sameProxy } from "./proxy.js";
26
31
  import { SettingsFile } from "./settings.js";
27
32
  import { bridgeSockets } from "./sockets.js";
28
- import { dispatchedOpener, hasWebSocket, proxiedDispatcher, unverifiedDispatcher, webSocketOpener, } from "./websocket-connections.js";
33
+ import { sandbox, setListening, setSettings, settings, setWindow, TITLE, trusted, window, } from "./state.js";
34
+ import { logSockets, netLog, network, proxied, proxy, trace, via, warn, webSocketOpen, } from "./switches.js";
29
35
  import { bridgeWebSockets } from "./websockets.js";
30
36
  const preload = fileURLToPath(new URL("../preload/preload.cjs", import.meta.url));
31
- const promptPreload = fileURLToPath(new URL("../preload/prompt.cjs", import.meta.url));
32
37
  // The window's and the taskbar's icon, the project's, where the platform takes one from the window.
33
38
  const icon = fileURLToPath(new URL("../../static/icon.png", import.meta.url));
34
- const PROMPT_URL = `${APP_ORIGIN}/open-url.html`;
35
- const TITLE = "swf2es";
36
39
  // The tests keep their settings apart from the user's.
37
40
  const userData = process.env.SWF2ES_DESKTOP_USER_DATA;
38
41
  if (userData) {
@@ -44,140 +47,8 @@ const build = /^[\w.+-]{1,40}$/.test(process.env.SWF2ES_DESKTOP_BUILD ?? "")
44
47
  : null;
45
48
  registerScheme();
46
49
  app.enableSandbox();
47
- /** --trace: what the page logs, a SWF's trace() among it, goes to the terminal. */
48
- const trace = process.argv.includes("--trace");
49
- // Each request and socket of the SWF's said on stderr: the page's DevTools show none of them.
50
- // --log-sockets says what each socket sends and receives as well, and so the rest too.
51
- const logSockets = process.argv.includes("--log-sockets");
52
- const logNetwork = logSockets || process.argv.includes("--log-network");
53
- const netLog = logNetwork ? (line) => process.stderr.write(`${line}\n`) : null;
54
- const sandbox = new Sandbox();
55
- /** The SWF's http and https requests, judged as Flash's sandboxes had them. */
56
- /**
57
- * --ignore-certificate-errors[=host,...]: HTTPS servers whose certificates go
58
- * unchecked, for one's own with a certificate it signed itself. For this
59
- * run alone, never kept, and said aloud: anyone on the way to such a host
60
- * can read and change what the SWF loads from it.
61
- */
62
- const unverified = unverifiedHosts(process.argv.slice(1));
63
- if (unverified) {
64
- process.stderr.write(unverified.all
65
- ? "swf2es: --ignore-certificate-errors: no HTTPS or WSS server's certificate is verified\n"
66
- : `swf2es: --ignore-certificate-errors: certificates are not verified for ${unverified.hosts.join(", ") || "no host"}\n`);
67
- }
68
- const unverifiedHost = (host) => !!unverified && (unverified.all || unverified.hosts.includes(host.toLowerCase()));
69
- /**
70
- * --proxy=<url>: what the SWF's requests, sockets and WebSockets go
71
- * through. For this run alone, never kept, and said aloud, without its
72
- * credentials. One that does not parse stops the app rather than letting
73
- * it run without.
74
- */
75
- const proxied = proxyArgument(process.argv.slice(1));
76
- if (proxied && "error" in proxied) {
77
- process.stderr.write(`swf2es: ${proxied.error}\n`);
78
- process.exit(2);
79
- }
80
- const proxy = proxied?.proxy ?? null;
81
- if (proxy) {
82
- process.stderr.write(`swf2es: --proxy: the SWF's requests, sockets and WebSockets go through ${describeProxy(proxy)}\n`);
83
- }
84
- /** What --log-network adds to a line of a connection that went through the proxy. */
85
- const via = proxy ? " via proxy" : "";
86
- const warn = (message) => process.stderr.write(`swf2es: ${message}\n`);
87
- const network = new Network({ unverified: unverifiedHost, proxy });
88
- // What opens an AIR application's WebSockets: the global's client, a wss: server's certificate
89
- // unchecked where the switch names its host; or every one through the proxy.
90
- const insecureWebSockets = unverified && !proxy ? unverifiedDispatcher() : null;
91
- if (unverified && !proxy && !insecureWebSockets) {
92
- process.stderr.write("swf2es: --ignore-certificate-errors: a WebSocket's certificate is verified all the same\n");
93
- }
94
- const proxiedWebSockets = proxy ? proxiedDispatcher(proxy, unverifiedHost, warn) : null;
95
- // Never past the proxy: where none can be made, no WebSocket opens.
96
- const webSocketOpen = proxy
97
- ? proxiedWebSockets
98
- ? dispatchedOpener(proxiedWebSockets)
99
- : () => {
100
- throw new Error("no WebSocket through the proxy");
101
- }
102
- : insecureWebSockets
103
- ? webSocketOpener(unverifiedHost, insecureWebSockets)
104
- : undefined;
105
- if (proxy && !proxiedWebSockets) {
106
- process.stderr.write("swf2es: --proxy: this Electron's WebSocket cannot go through a proxy\n");
107
- }
108
50
  /** What the page's requests under way abort with, by the preload's number. */
109
51
  const requests = new Map();
110
- let settings;
111
- /** What the network judges the movie playing as; null for none, or one without networking. */
112
- let networkMovie = null;
113
- let window = null;
114
- /** The dialog that asks for a URL, while it is open. */
115
- let prompt = null;
116
- let current = null;
117
- /** The servers the SWF playing was allowed, or denied, a socket to until it closes: "host:port". */
118
- let allowedOnce = new Set();
119
- let deniedOnce = new Set();
120
- /** The prompts on screen, by "host:port": a second connection there waits on the first's answer. */
121
- const asking = new Map();
122
- /**
123
- * The page has asked to start and listens for what opens: until then, start() hands it the movie.
124
- */
125
- let listening = false;
126
- /** The longest URL a SWF is opened by. */
127
- const MAX_URL = 8192;
128
- /** The AIR application playing, as its command line asked for it; null for none. */
129
- let air = null;
130
- /** The EncryptedLocalStore of the AIR application playing, if one is. */
131
- let encryptedItems = null;
132
- /**
133
- * `text` as the URL of a remote SWF: http or https, with a host and no
134
- * credentials, its fragment dropped; null if it is not one.
135
- */
136
- function remoteUrl(text) {
137
- if (text.length > MAX_URL) {
138
- return null;
139
- }
140
- let url;
141
- try {
142
- url = new URL(text.trim());
143
- }
144
- catch {
145
- return null;
146
- }
147
- if ((url.protocol !== "http:" && url.protocol !== "https:") || !url.hostname) {
148
- return null;
149
- }
150
- if (url.username || url.password) {
151
- return null;
152
- }
153
- url.hash = "";
154
- return url;
155
- }
156
- /**
157
- * The SWF named on a command line, from `cwd`: the last argument after the
158
- * app's own that is an http or https URL or names a .swf file there is.
159
- * Chromium's and Electron's switches may take values of their own
160
- * (`--switch value`), so the first argument that is not a switch could be one.
161
- */
162
- function movieFromArguments(argv, cwd = process.cwd()) {
163
- const named = argv
164
- .slice(process.defaultApp ? 2 : 1)
165
- .filter((a) => !a.startsWith("-"))
166
- .map((a) => remoteUrl(a)?.href ?? (/\.swf$/i.test(a) ? resolve(cwd, a) : null))
167
- .filter((a) => a !== null);
168
- return (named.reverse().find((a) => remoteUrl(a) || statSync(a, { throwIfNoEntry: false })?.isFile()) ??
169
- null);
170
- }
171
- /** Whether a message comes from the window's own page, on swf2es://app/. */
172
- function trusted(event) {
173
- const frame = event.senderFrame;
174
- return (window !== null &&
175
- event.sender === window.webContents &&
176
- frame !== null &&
177
- frame.parent === null &&
178
- // Node's URL gives a scheme it does not know no origin: compare the text.
179
- frame.url.startsWith(`${APP_ORIGIN}/`));
180
- }
181
52
  /** How long after a click or a key a SWF may open a page: Chromium's transient activation. */
182
53
  const GESTURE_MS = 5000;
183
54
  /** The least time between two pages opened, whatever the gestures. */
@@ -219,432 +90,6 @@ function openExternal(url) {
219
90
  openedAt = now;
220
91
  void shell.openExternal(url);
221
92
  }
222
- /**
223
- * How much of a SWF the main process reads to choose its sandbox: far more than FileAttributes
224
- * takes compressed.
225
- */
226
- const START = 64 * 1024;
227
- const USE_NETWORK = 0x01;
228
- /**
229
- * The first `length` bytes of the file at `path`, or as many as it has, at least `least`; null
230
- * otherwise.
231
- */
232
- async function readStart(path, length, least = 8) {
233
- try {
234
- const file = await openFile(path);
235
- try {
236
- const bytes = new Uint8Array(length);
237
- const { bytesRead } = await file.read(bytes, 0, length, 0);
238
- return bytesRead >= Math.min(least, length) ? bytes.subarray(0, bytesRead) : null;
239
- }
240
- finally {
241
- await file.close();
242
- }
243
- }
244
- catch {
245
- return null;
246
- }
247
- }
248
- function refuse(message, detail) {
249
- if (window) {
250
- void dialog.showMessageBox(window, { type: "error", message, detail });
251
- }
252
- }
253
- /** Open what the recent list, the command line or a reload names: a URL, a SWF's path or a descriptor's. */
254
- function openEntry(entry) {
255
- if (remoteUrl(entry)) {
256
- return openUrl(entry);
257
- }
258
- // A descriptor reopened from the recent list or a reload, with the arguments it was run with.
259
- if (/\.xml$/i.test(entry)) {
260
- return openAir(air?.descriptor === entry ? air : { descriptor: entry, root: null, arguments: [] }, process.cwd());
261
- }
262
- return open(entry);
263
- }
264
- /**
265
- * Run the AIR application `command` names, as adl does: its content SWF in
266
- * the window, at the descriptor's size, in AIR's application sandbox, with
267
- * its launch (shared/api.ts AirLaunch), from `cwd`.
268
- */
269
- async function openAir(command, cwd) {
270
- const name = basename(command.descriptor);
271
- let opened;
272
- try {
273
- opened = airLaunch(command, cwd, app.getPath("userData"), {
274
- documents: app.getPath("documents"),
275
- desktop: app.getPath("desktop"),
276
- cache: app.getPath("temp"),
277
- });
278
- }
279
- catch (error) {
280
- refuse(`${name} could not be opened.`, String(error));
281
- return;
282
- }
283
- const head = await readStart(opened.content, START);
284
- const signature = head ? String.fromCharCode(...head.subarray(0, 3)) : "";
285
- if (!head || (signature !== "FWS" && signature !== "CWS" && signature !== "ZWS")) {
286
- refuse(`${name}'s content is not a SWF.`, opened.content);
287
- return;
288
- }
289
- const content = await realpath(opened.content);
290
- sandbox.playApplication(content);
291
- air = command;
292
- // Beside its storage directory, by its id, as AIR keeps it; encrypted by the system's keychain where it has one.
293
- encryptedItems = new EncryptedItems(join(dirname(opened.launch.directories.applicationStorage), "EncryptedLocalStore"), safeStorage.isEncryptionAvailable()
294
- ? {
295
- encrypt: (text) => new Uint8Array(safeStorage.encryptString(text)),
296
- decrypt: (data) => safeStorage.decryptString(Buffer.from(data)),
297
- }
298
- : null);
299
- if (window && opened.width && opened.height) {
300
- window.setSize(opened.width, opened.height);
301
- }
302
- // Played from app:/, as AIR plays it: its relative URLs, and loaderInfo.url, are the application's.
303
- const url = `app:/${opened.relative
304
- .split("/")
305
- .filter((s) => s !== "")
306
- .map(encodeURIComponent)
307
- .join("/")}`;
308
- show(command.descriptor, { url, name: opened.title, sandbox: "application", air: opened.launch }, { kind: "application" });
309
- }
310
- /** Play `movie` in the window, in place of what plays: what the user opened, as `entry`. */
311
- function show(entry, movie, onNetwork) {
312
- current = { path: entry, movie };
313
- playOnNetwork(onNetwork);
314
- allowedOnce = new Set();
315
- deniedOnce = new Set();
316
- settings.opened(entry);
317
- buildMenu();
318
- if (window) {
319
- // An AIR application's window is its own: its title alone, as adl shows it.
320
- window.setTitle(movie.air ? movie.name : `${movie.name} — ${TITLE}`);
321
- if (listening) {
322
- window.webContents.send("desktop:open", movie);
323
- }
324
- }
325
- }
326
- /**
327
- * Play the SWF at `text`, an http or https URL, in Flash's remote sandbox:
328
- * no local file, and the network as its origin's (network.ts). The page
329
- * fetches it through the main process like anything it loads.
330
- */
331
- async function openUrl(text) {
332
- const url = remoteUrl(text);
333
- if (!url) {
334
- refuse("Only an http or https URL opens.", text.slice(0, 200));
335
- return;
336
- }
337
- const last = url.pathname.split("/").pop() ?? "";
338
- let name;
339
- try {
340
- name = decodeURIComponent(last) || url.host;
341
- }
342
- catch {
343
- name = last;
344
- }
345
- air = null;
346
- sandbox.playRemote(url.href);
347
- show(url.href, { url: url.href, name, sandbox: "remote" }, { kind: "remote", url: url.href });
348
- }
349
- /**
350
- * Open the file the user chose in the dialog, dropped on the window or
351
- * opened with the app: an AIR application's descriptor (`.xml`) runs the
352
- * application, anything else is played as a SWF.
353
- */
354
- async function openChosen(path) {
355
- if (!/\.xml$/i.test(path)) {
356
- return open(path);
357
- }
358
- // An application has an installed program's reach, which a file chosen in
359
- // passing should not get unasked: asked once, until it leaves the recent list.
360
- if (window && !settings.settings.recent.includes(path)) {
361
- const { response } = await dialog.showMessageBox(window, {
362
- type: "question",
363
- message: `Run ${basename(path)} as an AIR application?`,
364
- detail: "An AIR application reads and writes your files and reaches the network unasked, as an installed program does. Run only one you trust.",
365
- buttons: ["Run", "Cancel"],
366
- defaultId: 1,
367
- cancelId: 1,
368
- });
369
- if (response !== 0) {
370
- return;
371
- }
372
- }
373
- // Run from its own directory, with no arguments, as one started by its icon is.
374
- return openAir({ descriptor: path, root: null, arguments: [] }, dirname(path));
375
- }
376
- /** Play the SWF at `path` in the window, in place of what plays, in its sandbox. */
377
- async function open(path) {
378
- const name = basename(path);
379
- // Its start alone: the main process never decompresses a SWF whole, whose
380
- // header may name 4 GB. FileAttributes, the first tag, lies in it.
381
- const head = await readStart(path, START);
382
- const signature = head ? String.fromCharCode(...head.subarray(0, 3)) : "";
383
- if (!head || (signature !== "FWS" && signature !== "CWS" && signature !== "ZWS")) {
384
- refuse(`${name} is not a SWF.`, path);
385
- return;
386
- }
387
- const useNetwork = (fileAttributes(head) & USE_NETWORK) !== 0;
388
- let url;
389
- try {
390
- air = null;
391
- url = sandbox.play(path, useNetwork);
392
- }
393
- catch (error) {
394
- // Gone, or unreadable, since it was read.
395
- refuse(`${name} could not be opened.`, String(error));
396
- return;
397
- }
398
- app.addRecentDocument(path);
399
- show(path, { url, name, sandbox: useNetwork ? "localWithNetwork" : "localWithFile" }, useNetwork ? { kind: "local" } : null);
400
- }
401
- /** Judge requests for `movie` from now on: the last one's are aborted. */
402
- function playOnNetwork(movie) {
403
- networkMovie = movie;
404
- network.play(movie);
405
- }
406
- function closeMovie() {
407
- current = null;
408
- air = null;
409
- sandbox.stop();
410
- playOnNetwork(null);
411
- buildMenu();
412
- window?.setTitle(TITLE);
413
- if (listening) {
414
- window?.webContents.send("desktop:close");
415
- }
416
- }
417
- /**
418
- * Whether the SWF playing may connect a socket to `host`:`port`, and where
419
- * to: only with the network, and only as the user allows, once or always
420
- * for that SWF, when it first asks for that server; and for a remote SWF,
421
- * as that server's socket policy allows, at the address it came from.
422
- * Denied where no one can be asked.
423
- */
424
- async function permitSocket(host, port) {
425
- // AIR's application sandbox connects where it likes, unasked, with no socket policy.
426
- if (sandbox.application) {
427
- return {};
428
- }
429
- const playing = current;
430
- const swf = sandbox.swf;
431
- const endpoint = `${host}:${port}`;
432
- if (!playing || !swf || !sandbox.networkAllowed()) {
433
- process.stderr.write(`swf2es: no socket to ${endpoint}: the SWF has no network\n`);
434
- return null;
435
- }
436
- if (!(await allowedByUser(playing, swf, endpoint))) {
437
- return null;
438
- }
439
- if (networkMovie?.kind !== "remote") {
440
- return {};
441
- }
442
- try {
443
- return { address: await network.socketAddress(host, port) };
444
- }
445
- catch (error) {
446
- process.stderr.write(`swf2es: no socket to ${endpoint}: ${error.message}\n`);
447
- return null;
448
- }
449
- }
450
- /** Whether the user allows the SWF playing a socket to `endpoint`, asked once a server. */
451
- async function allowedByUser(playing, swf, endpoint) {
452
- if (settings.socketAllowed(swf, endpoint) || allowedOnce.has(endpoint)) {
453
- return true;
454
- }
455
- if (deniedOnce.has(endpoint)) {
456
- return false;
457
- }
458
- let answer = asking.get(endpoint);
459
- if (!answer) {
460
- answer = askForSocket(playing.movie.name, swf, endpoint).finally(() => asking.delete(endpoint));
461
- asking.set(endpoint, answer);
462
- }
463
- return (await answer) && current === playing;
464
- }
465
- async function askForSocket(name, swf, endpoint) {
466
- if (!window) {
467
- return false;
468
- }
469
- const playing = current;
470
- let response;
471
- try {
472
- ({ response } = await dialog.showMessageBox(window, {
473
- type: "question",
474
- message: `${name} asks to connect to ${endpoint}.`,
475
- detail: "A socket lets the SWF send that server whatever it has. " +
476
- "Allow it only for a server you trust.",
477
- buttons: ["Allow Once", "Always Allow for This SWF", "Deny"],
478
- defaultId: 2,
479
- cancelId: 2,
480
- noLink: true,
481
- }));
482
- }
483
- catch {
484
- return false;
485
- }
486
- if (current !== playing) {
487
- return false;
488
- }
489
- switch (response) {
490
- case 0:
491
- allowedOnce.add(endpoint);
492
- return true;
493
- case 1:
494
- if (!settings.allowSocket(swf, endpoint)) {
495
- allowedOnce.add(endpoint);
496
- }
497
- return true;
498
- default:
499
- deniedOnce.add(endpoint);
500
- return false;
501
- }
502
- }
503
- /**
504
- * Whether what plays may open a WebSocket to `url`: an AIR application,
505
- * unasked and anywhere, as AIR's application sandbox connects and as its
506
- * sockets do here. air.net.WebSocket is AIR's alone, so nothing else has one.
507
- */
508
- function permitWebSocket(url) {
509
- if (sandbox.application && hasWebSocket() && (!proxy || proxiedWebSockets)) {
510
- return true;
511
- }
512
- const why = !sandbox.application
513
- ? "only an AIR application has one"
514
- : !hasWebSocket()
515
- ? "this Electron has no WebSocket client"
516
- : "this Electron's WebSocket cannot go through the proxy";
517
- process.stderr.write(`swf2es: no WebSocket to ${url.slice(0, 2048)}: ${why}\n`);
518
- return false;
519
- }
520
- async function showOpenDialog() {
521
- if (!window) {
522
- return;
523
- }
524
- const result = await dialog.showOpenDialog(window, {
525
- title: "Open a SWF or an AIR application",
526
- properties: ["openFile"],
527
- filters: [
528
- { name: "SWF and AIR application descriptors", extensions: ["swf", "xml"] },
529
- { name: "All files", extensions: ["*"] },
530
- ],
531
- });
532
- if (!result.canceled && result.filePaths[0]) {
533
- await openChosen(result.filePaths[0]);
534
- }
535
- }
536
- /** Ask for a library, check that it is ABC, keep its path, and start the page again with it. */
537
- async function chooseLibrary(name) {
538
- if (!window) {
539
- return;
540
- }
541
- const result = await dialog.showOpenDialog(window, {
542
- title: `Choose ${name}.abc`,
543
- properties: ["openFile"],
544
- filters: [
545
- { name: "ActionScript bytecode", extensions: ["abc"] },
546
- { name: "All files", extensions: ["*"] },
547
- ],
548
- });
549
- const path = result.filePaths[0];
550
- if (result.canceled || !path) {
551
- return;
552
- }
553
- // An ABC starts with its minor version, then its major one, 46.
554
- const head = await readStart(path, 4);
555
- if (head?.[2] !== 46 || head[3] !== 0) {
556
- refuse(`${basename(path)} is not an ABC file.`, `swf2es needs ${name}.abc itself.`);
557
- return;
558
- }
559
- if (!settings.setLibrary(name, path)) {
560
- refuse("The settings could not be saved.", `swf2es uses ${path} until it quits.`);
561
- }
562
- buildMenu();
563
- // The page fetched the libraries once; a fresh page fetches the new one.
564
- window.webContents.reload();
565
- }
566
- /** Play on the library the app ships again, in place of one the user chose. */
567
- function resetLibrary(name) {
568
- if (!settings.clearLibrary(name)) {
569
- refuse("The settings could not be saved.", `swf2es uses its own ${name} until it quits.`);
570
- }
571
- buildMenu();
572
- window?.webContents.reload();
573
- }
574
- function buildMenu() {
575
- const recent = settings.settings.recent;
576
- const template = [
577
- ...(process.platform === "darwin" ? [{ role: "appMenu" }] : []),
578
- {
579
- label: "&File",
580
- submenu: [
581
- { label: "&Open…", accelerator: "CmdOrCtrl+O", click: () => void showOpenDialog() },
582
- { label: "Open &URL…", accelerator: "CmdOrCtrl+L", click: showUrlDialog },
583
- {
584
- label: "Open &Recent",
585
- submenu: [
586
- ...recent.map((entry) => ({ label: entry, click: () => void openEntry(entry) })),
587
- ...(recent.length > 0 ? [{ type: "separator" }] : []),
588
- {
589
- label: "Clear Recent",
590
- enabled: recent.length > 0,
591
- click: () => {
592
- settings.clearRecent();
593
- app.clearRecentDocuments();
594
- buildMenu();
595
- },
596
- },
597
- ],
598
- },
599
- {
600
- id: "close-movie",
601
- label: "&Close Movie",
602
- accelerator: "CmdOrCtrl+W",
603
- enabled: !!current,
604
- click: closeMovie,
605
- },
606
- { type: "separator" },
607
- { label: "Choose playerglobal.abc…", click: () => void chooseLibrary("playerglobal") },
608
- {
609
- label: "Use the App's playerglobal",
610
- enabled: !!settings.settings.playerglobal,
611
- click: () => resetLibrary("playerglobal"),
612
- },
613
- { type: "separator" },
614
- { role: "quit" },
615
- ],
616
- },
617
- {
618
- label: "&View",
619
- submenu: [
620
- {
621
- id: "reload-movie",
622
- label: "&Reload Movie",
623
- accelerator: "CmdOrCtrl+R",
624
- enabled: !!current,
625
- click: () => current && void openEntry(current.path),
626
- },
627
- { role: "togglefullscreen", accelerator: "F11" },
628
- { type: "separator" },
629
- {
630
- id: "stage3d",
631
- label: "Stage3D",
632
- type: "checkbox",
633
- checked: settings.stage3D,
634
- click: (item) => {
635
- if (!settings.setStage3D(item.checked)) {
636
- refuse("The settings could not be saved.", "The Stage3D choice holds until you quit.");
637
- }
638
- window?.webContents.reload();
639
- },
640
- },
641
- { type: "separator" },
642
- { role: "toggleDevTools" },
643
- ],
644
- },
645
- ];
646
- Menu.setApplicationMenu(Menu.buildFromTemplate(template));
647
- }
648
93
  /**
649
94
  * The window's page may go nowhere but swf2es://app/, and open nothing but the system's browser.
650
95
  */
@@ -664,7 +109,7 @@ function guard(contents) {
664
109
  });
665
110
  contents.on("did-start-navigation", (details) => {
666
111
  if (details.isMainFrame && !details.isSameDocument) {
667
- listening = false;
112
+ setListening(false);
668
113
  // The page's requests go with it; the next page asks for the movie again.
669
114
  network.play(networkMovie);
670
115
  requests.clear();
@@ -677,63 +122,8 @@ function guard(contents) {
677
122
  });
678
123
  }
679
124
  }
680
- /**
681
- * The dialog that asks for a SWF's URL: a window of its own, apart from the
682
- * page, so that only the user, and never a SWF, opens one by its URL.
683
- */
684
- function showUrlDialog() {
685
- if (!window) {
686
- return;
687
- }
688
- if (prompt) {
689
- prompt.focus();
690
- return;
691
- }
692
- const dialogWindow = new BrowserWindow({
693
- parent: window,
694
- modal: true,
695
- width: 560,
696
- height: 180,
697
- resizable: false,
698
- minimizable: false,
699
- maximizable: false,
700
- fullscreenable: false,
701
- title: "Open URL",
702
- show: false,
703
- webPreferences: {
704
- preload: promptPreload,
705
- contextIsolation: true,
706
- sandbox: true,
707
- nodeIntegration: false,
708
- webSecurity: true,
709
- spellcheck: false,
710
- },
711
- });
712
- prompt = dialogWindow;
713
- dialogWindow.setMenu(null);
714
- dialogWindow.on("page-title-updated", (event) => event.preventDefault());
715
- dialogWindow.once("ready-to-show", () => dialogWindow.show());
716
- dialogWindow.on("closed", () => {
717
- if (prompt === dialogWindow) {
718
- prompt = null;
719
- }
720
- });
721
- const contents = dialogWindow.webContents;
722
- contents.on("will-navigate", (event) => event.preventDefault());
723
- contents.setWindowOpenHandler(() => ({ action: "deny" }));
724
- void dialogWindow.loadURL(PROMPT_URL);
725
- }
726
- /** Whether a message comes from the URL dialog's own page. */
727
- function fromPrompt(event) {
728
- const frame = event.senderFrame;
729
- return (prompt !== null &&
730
- event.sender === prompt.webContents &&
731
- frame !== null &&
732
- frame.parent === null &&
733
- frame.url === PROMPT_URL);
734
- }
735
125
  function createWindow() {
736
- window = new BrowserWindow({
126
+ const window = new BrowserWindow({
737
127
  width: 1024,
738
128
  height: 768,
739
129
  title: TITLE,
@@ -745,12 +135,18 @@ function createWindow() {
745
135
  nodeIntegration: false,
746
136
  webSecurity: true,
747
137
  spellcheck: false,
138
+ // A SWF plays on, as in Flash, while the window is minimized or covered: throttled, its
139
+ // timers would fire once a second at most, and a covered window on Windows would count
140
+ // as hidden. A game's server takes the silence for a lost client, and what the SWF then
141
+ // answers all at once for a flood (Playback's timer, in @swf2es/web).
142
+ backgroundThrottling: false,
748
143
  },
749
144
  });
145
+ setWindow(window);
750
146
  // The title is the movie's, set here, not the page's <title>.
751
147
  window.on("page-title-updated", (event) => event.preventDefault());
752
148
  window.on("closed", () => {
753
- window = null;
149
+ setWindow(null);
754
150
  });
755
151
  // The page hears of full screen however it came, a SWF's Stage.displayState or the menu's F11.
756
152
  window.on("enter-full-screen", () => window?.webContents.send("desktop:full-screen", true));
@@ -770,7 +166,7 @@ function listen() {
770
166
  if (!trusted(event)) {
771
167
  throw new Error("not the app's page");
772
168
  }
773
- listening = true;
169
+ setListening(true);
774
170
  return {
775
171
  stage3D: settings.stage3D,
776
172
  libraries: settings.libraries(),
@@ -850,162 +246,6 @@ function listen() {
850
246
  });
851
247
  listenForAir();
852
248
  }
853
- /** The window as an AIR application's NativeWindow reads it. */
854
- function windowState(w) {
855
- const bounds = w.getBounds();
856
- const content = w.getContentBounds();
857
- return {
858
- bounds,
859
- contentOffset: { x: content.x - bounds.x, y: content.y - bounds.y },
860
- title: w.getTitle(),
861
- displayState: w.isMinimized() ? "minimized" : w.isMaximized() ? "maximized" : "normal",
862
- active: w.isFocused(),
863
- visible: w.isVisible(),
864
- };
865
- }
866
- /** The displays as an AIR application's Screen reads them: the work area is past the system's bars. */
867
- function screenStates() {
868
- const primary = screen.getPrimaryDisplay().id;
869
- return screen.getAllDisplays().map((display) => ({
870
- bounds: display.bounds,
871
- visibleBounds: display.workArea,
872
- colorDepth: display.colorDepth,
873
- main: display.id === primary,
874
- }));
875
- }
876
- /**
877
- * An AIR application's reach, shared/api.ts DesktopAir: the file system
878
- * and the window, only while one the user opened plays and only for its
879
- * page. The file system is read synchronously where File reads it so.
880
- */
881
- function listenForAir() {
882
- const playing = (event) => trusted(event) && sandbox.application;
883
- ipcMain.on("air:stat", (event, path) => {
884
- event.returnValue = playing(event) && typeof path === "string" ? statOf(path) : null;
885
- });
886
- ipcMain.on("air:list", (event, path) => {
887
- event.returnValue = playing(event) && typeof path === "string" ? listOf(path) : null;
888
- });
889
- // FileStream's open, which reads a file at once, as stat does: only once the application plays.
890
- ipcMain.on("air:read-sync", (event, path) => {
891
- event.returnValue = playing(event) && typeof path === "string" ? readOf(path) : null;
892
- });
893
- // EncryptedLocalStore, at once as its methods are: only the application playing's.
894
- const items = (event) => (playing(event) ? encryptedItems : null);
895
- ipcMain.on("air:item-get", (event, name) => {
896
- event.returnValue = typeof name === "string" ? (items(event)?.get(name) ?? null) : null;
897
- });
898
- ipcMain.on("air:item-set", (event, name, bytes) => {
899
- if (typeof name === "string" && bytes instanceof Uint8Array) {
900
- items(event)?.set(name, bytes);
901
- }
902
- event.returnValue = null;
903
- });
904
- ipcMain.on("air:item-remove", (event, name) => {
905
- if (typeof name === "string") {
906
- items(event)?.remove(name);
907
- }
908
- event.returnValue = null;
909
- });
910
- ipcMain.on("air:items-reset", (event) => {
911
- items(event)?.reset();
912
- event.returnValue = null;
913
- });
914
- // FileStream's writes and File's changes, at once as adl's: only once the application plays.
915
- ipcMain.on("air:write-sync", (event, path, bytes, offset, length) => {
916
- event.returnValue =
917
- playing(event) &&
918
- typeof path === "string" &&
919
- bytes instanceof Uint8Array &&
920
- Number.isSafeInteger(offset) &&
921
- Number.isSafeInteger(length) &&
922
- offset >= 0 &&
923
- length >= 0 &&
924
- writeOf(path, bytes, offset, length);
925
- });
926
- ipcMain.on("air:create-directory", (event, path) => {
927
- event.returnValue = playing(event) && typeof path === "string" && createDirectoryOf(path);
928
- });
929
- ipcMain.on("air:remove", (event, path) => {
930
- event.returnValue = playing(event) && typeof path === "string" && removeOf(path);
931
- });
932
- ipcMain.on("air:move", (event, from, to) => {
933
- event.returnValue =
934
- playing(event) && typeof from === "string" && typeof to === "string" && moveOf(from, to);
935
- });
936
- // A read is what starts the application's grant: the page reads its content SWF first.
937
- ipcMain.handle("air:read", async (event, path) => {
938
- if (!trusted(event) || typeof path !== "string" || !air) {
939
- return null;
940
- }
941
- try {
942
- const real = await realpath(resolveCase(path));
943
- return sandbox.startApplication(real) ? new Uint8Array(await readFile(real)) : null;
944
- }
945
- catch {
946
- return null;
947
- }
948
- });
949
- ipcMain.on("air:window", (event) => {
950
- event.returnValue = playing(event) && window ? windowState(window) : null;
951
- });
952
- ipcMain.on("air:screens", (event) => {
953
- event.returnValue = playing(event) ? screenStates() : [];
954
- });
955
- ipcMain.on("air:set-bounds", (event, bounds) => {
956
- const b = bounds;
957
- const n = (v) => (typeof v === "number" && Number.isFinite(v) ? Math.round(v) : null);
958
- const [x, y, width, height] = [n(b?.x), n(b?.y), n(b?.width), n(b?.height)];
959
- if (playing(event) &&
960
- window &&
961
- x !== null &&
962
- y !== null &&
963
- width &&
964
- height &&
965
- width > 0 &&
966
- height > 0) {
967
- window.setBounds({ x, y, width, height });
968
- }
969
- });
970
- ipcMain.on("air:set-title", (event, title) => {
971
- if (playing(event) && window && typeof title === "string") {
972
- window.setTitle(title.slice(0, 1024));
973
- }
974
- });
975
- ipcMain.on("air:window-do", (event, what) => {
976
- if (!playing(event) || !window) {
977
- return;
978
- }
979
- switch (what) {
980
- case "minimize":
981
- window.minimize();
982
- break;
983
- case "maximize":
984
- window.maximize();
985
- break;
986
- case "restore":
987
- window.restore();
988
- break;
989
- case "activate":
990
- window.focus();
991
- break;
992
- case "show":
993
- window.show();
994
- break;
995
- case "hide":
996
- window.hide();
997
- break;
998
- case "close":
999
- window.close();
1000
- break;
1001
- }
1002
- });
1003
- ipcMain.on("air:exit", (event, code) => {
1004
- if (playing(event)) {
1005
- app.exit(typeof code === "number" ? code | 0 : 0);
1006
- }
1007
- });
1008
- }
1009
249
  /**
1010
250
  * A request of the page's: the SWF's own, as the movie opened by its URL,
1011
251
  * or what the SWF playing loads, only with the network, as network.ts
@@ -1103,7 +343,7 @@ void app.whenReady().then(() => {
1103
343
  if (!first) {
1104
344
  return;
1105
345
  }
1106
- settings = new SettingsFile(app.getPath("userData"));
346
+ setSettings(new SettingsFile(app.getPath("userData")));
1107
347
  // A playerglobal chosen once is kept across versions of the app: said at each start, as a
1108
348
  // library older or newer than the app's own makes SWFs fail in ways nothing else explains.
1109
349
  const chosen = settings.libraries().playerglobal;