@swf2es/desktop 0.1.15 → 0.1.17

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 (86) hide show
  1. package/README.md +85 -12
  2. package/dist/launch/launch.js +4 -1
  3. package/dist/launch/launch.js.map +1 -1
  4. package/dist/main/air.d.ts +3 -3
  5. package/dist/main/air.d.ts.map +1 -1
  6. package/dist/main/air.js +17 -8
  7. package/dist/main/air.js.map +1 -1
  8. package/dist/main/case-paths.d.ts +12 -0
  9. package/dist/main/case-paths.d.ts.map +1 -0
  10. package/dist/main/case-paths.js +84 -0
  11. package/dist/main/case-paths.js.map +1 -0
  12. package/dist/main/connections.d.ts +20 -1
  13. package/dist/main/connections.d.ts.map +1 -1
  14. package/dist/main/connections.js +71 -8
  15. package/dist/main/connections.js.map +1 -1
  16. package/dist/main/main.js +133 -9
  17. package/dist/main/main.js.map +1 -1
  18. package/dist/main/netlog.d.ts +21 -0
  19. package/dist/main/netlog.d.ts.map +1 -0
  20. package/dist/main/netlog.js +90 -0
  21. package/dist/main/netlog.js.map +1 -0
  22. package/dist/main/network.d.ts +10 -0
  23. package/dist/main/network.d.ts.map +1 -1
  24. package/dist/main/network.js +111 -35
  25. package/dist/main/network.js.map +1 -1
  26. package/dist/main/proxy.d.ts +48 -0
  27. package/dist/main/proxy.d.ts.map +1 -0
  28. package/dist/main/proxy.js +402 -0
  29. package/dist/main/proxy.js.map +1 -0
  30. package/dist/main/sandbox.d.ts.map +1 -1
  31. package/dist/main/sandbox.js +2 -1
  32. package/dist/main/sandbox.js.map +1 -1
  33. package/dist/main/settings.d.ts +2 -0
  34. package/dist/main/settings.d.ts.map +1 -1
  35. package/dist/main/settings.js +4 -0
  36. package/dist/main/settings.js.map +1 -1
  37. package/dist/main/sockets.d.ts +6 -2
  38. package/dist/main/sockets.d.ts.map +1 -1
  39. package/dist/main/sockets.js +18 -3
  40. package/dist/main/sockets.js.map +1 -1
  41. package/dist/main/websocket-connections.d.ts +73 -0
  42. package/dist/main/websocket-connections.d.ts.map +1 -0
  43. package/dist/main/websocket-connections.js +266 -0
  44. package/dist/main/websocket-connections.js.map +1 -0
  45. package/dist/main/websockets.d.ts +12 -0
  46. package/dist/main/websockets.d.ts.map +1 -0
  47. package/dist/main/websockets.js +94 -0
  48. package/dist/main/websockets.js.map +1 -0
  49. package/dist/preload/preload.cjs +20 -0
  50. package/dist/preload/preload.cjs.map +1 -1
  51. package/dist/renderer/air.d.ts.map +1 -1
  52. package/dist/renderer/air.js +1 -0
  53. package/dist/renderer/air.js.map +1 -1
  54. package/dist/renderer/full-screen.d.ts +4 -0
  55. package/dist/renderer/full-screen.d.ts.map +1 -0
  56. package/dist/renderer/full-screen.js +32 -0
  57. package/dist/renderer/full-screen.js.map +1 -0
  58. package/dist/renderer/index.d.ts.map +1 -1
  59. package/dist/renderer/index.js +4 -0
  60. package/dist/renderer/index.js.map +1 -1
  61. package/dist/renderer/websockets.d.ts +4 -0
  62. package/dist/renderer/websockets.d.ts.map +1 -0
  63. package/dist/renderer/websockets.js +36 -0
  64. package/dist/renderer/websockets.js.map +1 -0
  65. package/dist/shared/api.d.ts +53 -0
  66. package/dist/shared/api.d.ts.map +1 -1
  67. package/package.json +7 -7
  68. package/src/launch/launch.ts +5 -2
  69. package/src/main/air.ts +18 -8
  70. package/src/main/case-paths.ts +97 -0
  71. package/src/main/connections.ts +90 -8
  72. package/src/main/main.ts +174 -10
  73. package/src/main/netlog.ts +108 -0
  74. package/src/main/network.ts +140 -37
  75. package/src/main/proxy.ts +497 -0
  76. package/src/main/sandbox.ts +2 -1
  77. package/src/main/settings.ts +5 -0
  78. package/src/main/sockets.ts +22 -3
  79. package/src/main/websocket-connections.ts +344 -0
  80. package/src/main/websockets.ts +117 -0
  81. package/src/preload/preload.cts +23 -0
  82. package/src/renderer/air.ts +1 -0
  83. package/src/renderer/full-screen.ts +44 -0
  84. package/src/renderer/index.ts +4 -0
  85. package/src/renderer/websockets.ts +43 -0
  86. package/src/shared/api.ts +40 -0
package/src/main/main.ts CHANGED
@@ -5,7 +5,8 @@
5
5
  // electron apps/desktop [--trace] [file.swf | http(s)://host/movie.swf]
6
6
  // electron apps/desktop [--trace] app.xml [root-dir] [-- arguments...]
7
7
  //
8
- // --ignore-certificate-errors[=host,...] leaves HTTPS certificates unchecked.
8
+ // --ignore-certificate-errors[=host,...] leaves HTTPS and WSS certificates unchecked.
9
+ // --proxy=socks5://[user:pass@]host:port (or http://...) sends the SWF's network through a proxy.
9
10
  //
10
11
  // The second runs an AIR application by its descriptor, as adl does.
11
12
  import { statSync } from "node:fs";
@@ -23,11 +24,13 @@ import {
23
24
  Menu,
24
25
  type MenuItemConstructorOptions,
25
26
  safeStorage,
27
+ screen,
26
28
  session,
27
29
  shell,
28
30
  type WebContents,
29
31
  } from "electron";
30
32
  import type {
33
+ AirScreenState,
31
34
  AirWindowState,
32
35
  LibraryName,
33
36
  NetworkResponse,
@@ -47,12 +50,24 @@ import {
47
50
  statOf,
48
51
  writeOf,
49
52
  } from "./air.ts";
53
+ import { resolveCase } from "./case-paths.ts";
50
54
  import { bridgeFiles } from "./files.ts";
55
+ import { failureLine, requestLine } from "./netlog.ts";
51
56
  import { type Movie, Network, Refused, unverifiedHosts } from "./network.ts";
52
57
  import { APP_ORIGIN, registerScheme, serve } from "./protocol.ts";
58
+ import { describeProxy, proxyArgument, sameProxy } from "./proxy.ts";
53
59
  import { Sandbox } from "./sandbox.ts";
54
60
  import { SettingsFile } from "./settings.ts";
55
61
  import { bridgeSockets } from "./sockets.ts";
62
+ import {
63
+ dispatchedOpener,
64
+ hasWebSocket,
65
+ type Opener,
66
+ proxiedDispatcher,
67
+ unverifiedDispatcher,
68
+ webSocketOpener,
69
+ } from "./websocket-connections.ts";
70
+ import { bridgeWebSockets } from "./websockets.ts";
56
71
 
57
72
  const preload = fileURLToPath(new URL("../preload/preload.cjs", import.meta.url));
58
73
  const promptPreload = fileURLToPath(new URL("../preload/prompt.cjs", import.meta.url));
@@ -77,6 +92,11 @@ app.enableSandbox();
77
92
 
78
93
  /** --trace: what the page logs, a SWF's trace() among it, goes to the terminal. */
79
94
  const trace = process.argv.includes("--trace");
95
+ // Each request and socket of the SWF's said on stderr: the page's DevTools show none of them.
96
+ // --log-sockets says what each socket sends and receives as well, and so the rest too.
97
+ const logSockets = process.argv.includes("--log-sockets");
98
+ const logNetwork = logSockets || process.argv.includes("--log-network");
99
+ const netLog = logNetwork ? (line: string) => process.stderr.write(`${line}\n`) : null;
80
100
  const sandbox = new Sandbox();
81
101
  /** The SWF's http and https requests, judged as Flash's sandboxes had them. */
82
102
  /**
@@ -89,15 +109,61 @@ const unverified = unverifiedHosts(process.argv.slice(1));
89
109
  if (unverified) {
90
110
  process.stderr.write(
91
111
  unverified.all
92
- ? "swf2es: --ignore-certificate-errors: no HTTPS server's certificate is verified\n"
112
+ ? "swf2es: --ignore-certificate-errors: no HTTPS or WSS server's certificate is verified\n"
93
113
  : `swf2es: --ignore-certificate-errors: certificates are not verified for ${unverified.hosts.join(", ") || "no host"}\n`,
94
114
  );
95
115
  }
96
116
 
97
- const network = new Network({
98
- unverified: (host) =>
99
- !!unverified && (unverified.all || unverified.hosts.includes(host.toLowerCase())),
100
- });
117
+ const unverifiedHost = (host: string) =>
118
+ !!unverified && (unverified.all || unverified.hosts.includes(host.toLowerCase()));
119
+
120
+ /**
121
+ * --proxy=<url>: what the SWF's requests, sockets and WebSockets go
122
+ * through. For this run alone, never kept, and said aloud, without its
123
+ * credentials. One that does not parse stops the app rather than letting
124
+ * it run without.
125
+ */
126
+ const proxied = proxyArgument(process.argv.slice(1));
127
+ if (proxied && "error" in proxied) {
128
+ process.stderr.write(`swf2es: ${proxied.error}\n`);
129
+ process.exit(2);
130
+ }
131
+
132
+ const proxy = proxied?.proxy ?? null;
133
+ if (proxy) {
134
+ process.stderr.write(
135
+ `swf2es: --proxy: the SWF's requests, sockets and WebSockets go through ${describeProxy(proxy)}\n`,
136
+ );
137
+ }
138
+
139
+ /** What --log-network adds to a line of a connection that went through the proxy. */
140
+ const via = proxy ? " via proxy" : "";
141
+ const warn = (message: string) => process.stderr.write(`swf2es: ${message}\n`);
142
+ const network = new Network({ unverified: unverifiedHost, proxy });
143
+ // What opens an AIR application's WebSockets: the global's client, a wss: server's certificate
144
+ // unchecked where the switch names its host; or every one through the proxy.
145
+ const insecureWebSockets = unverified && !proxy ? unverifiedDispatcher() : null;
146
+ if (unverified && !proxy && !insecureWebSockets) {
147
+ process.stderr.write(
148
+ "swf2es: --ignore-certificate-errors: a WebSocket's certificate is verified all the same\n",
149
+ );
150
+ }
151
+
152
+ const proxiedWebSockets = proxy ? proxiedDispatcher(proxy, unverifiedHost, warn) : null;
153
+ // Never past the proxy: where none can be made, no WebSocket opens.
154
+ const webSocketOpen: Opener | undefined = proxy
155
+ ? proxiedWebSockets
156
+ ? dispatchedOpener(proxiedWebSockets)
157
+ : () => {
158
+ throw new Error("no WebSocket through the proxy");
159
+ }
160
+ : insecureWebSockets
161
+ ? webSocketOpener(unverifiedHost, insecureWebSockets)
162
+ : undefined;
163
+ if (proxy && !proxiedWebSockets) {
164
+ process.stderr.write("swf2es: --proxy: this Electron's WebSocket cannot go through a proxy\n");
165
+ }
166
+
101
167
  /** What the page's requests under way abort with, by the preload's number. */
102
168
  const requests = new Map<number, AbortController>();
103
169
  let settings: SettingsFile;
@@ -560,6 +626,25 @@ async function askForSocket(name: string, swf: string, endpoint: string): Promis
560
626
  }
561
627
  }
562
628
 
629
+ /**
630
+ * Whether what plays may open a WebSocket to `url`: an AIR application,
631
+ * unasked and anywhere, as AIR's application sandbox connects and as its
632
+ * sockets do here. air.net.WebSocket is AIR's alone, so nothing else has one.
633
+ */
634
+ function permitWebSocket(url: string): boolean {
635
+ if (sandbox.application && hasWebSocket() && (!proxy || proxiedWebSockets)) {
636
+ return true;
637
+ }
638
+
639
+ const why = !sandbox.application
640
+ ? "only an AIR application has one"
641
+ : !hasWebSocket()
642
+ ? "this Electron has no WebSocket client"
643
+ : "this Electron's WebSocket cannot go through the proxy";
644
+ process.stderr.write(`swf2es: no WebSocket to ${url.slice(0, 2048)}: ${why}\n`);
645
+ return false;
646
+ }
647
+
563
648
  async function showOpenDialog(): Promise<void> {
564
649
  if (!window) {
565
650
  return;
@@ -608,10 +693,22 @@ async function chooseLibrary(name: LibraryName): Promise<void> {
608
693
  refuse("The settings could not be saved.", `swf2es uses ${path} until it quits.`);
609
694
  }
610
695
 
696
+ buildMenu();
697
+
611
698
  // The page fetched the libraries once; a fresh page fetches the new one.
612
699
  window.webContents.reload();
613
700
  }
614
701
 
702
+ /** Play on the library the app ships again, in place of one the user chose. */
703
+ function resetLibrary(name: LibraryName): void {
704
+ if (!settings.clearLibrary(name)) {
705
+ refuse("The settings could not be saved.", `swf2es uses its own ${name} until it quits.`);
706
+ }
707
+
708
+ buildMenu();
709
+ window?.webContents.reload();
710
+ }
711
+
615
712
  function buildMenu(): void {
616
713
  const recent = settings.settings.recent;
617
714
  const template: MenuItemConstructorOptions[] = [
@@ -646,6 +743,11 @@ function buildMenu(): void {
646
743
  },
647
744
  { type: "separator" },
648
745
  { label: "Choose playerglobal.abc…", click: () => void chooseLibrary("playerglobal") },
746
+ {
747
+ label: "Use the App's playerglobal",
748
+ enabled: !!settings.settings.playerglobal,
749
+ click: () => resetLibrary("playerglobal"),
750
+ },
649
751
  { type: "separator" },
650
752
  { role: "quit" },
651
753
  ],
@@ -800,6 +902,9 @@ function createWindow(): void {
800
902
  window.on("closed", () => {
801
903
  window = null;
802
904
  });
905
+ // The page hears of full screen however it came, a SWF's Stage.displayState or the menu's F11.
906
+ window.on("enter-full-screen", () => window?.webContents.send("desktop:full-screen", true));
907
+ window.on("leave-full-screen", () => window?.webContents.send("desktop:full-screen", false));
803
908
  guard(window.webContents);
804
909
  void window.loadURL(`${APP_ORIGIN}/`);
805
910
  }
@@ -831,6 +936,12 @@ function listen(): void {
831
936
  build,
832
937
  };
833
938
  });
939
+ // Stage.displayState of a SWF the user opened: the window's full screen, asked with no gesture.
940
+ ipcMain.on("desktop:full-screen", (event, fullScreen: unknown) => {
941
+ if (trusted(event) && current && window) {
942
+ window.setFullScreen(fullScreen === true);
943
+ }
944
+ });
834
945
  ipcMain.on("desktop:open-dialog", (event) => {
835
946
  if (trusted(event)) {
836
947
  void showOpenDialog();
@@ -869,7 +980,8 @@ function listen(): void {
869
980
  void chooseLibrary(name);
870
981
  }
871
982
  });
872
- bridgeSockets(trusted, permitSocket);
983
+ bridgeSockets(trusted, permitSocket, netLog, logSockets, { proxy, warn });
984
+ bridgeWebSockets(trusted, permitWebSocket, webSocketOpen, netLog, logSockets, via);
873
985
  ipcMain.handle("net:fetch", (event, id: unknown, request: unknown) => {
874
986
  if (!trusted(event) || typeof id !== "number") {
875
987
  throw new Error("not the app's page");
@@ -917,6 +1029,17 @@ function windowState(w: BrowserWindow): AirWindowState {
917
1029
  };
918
1030
  }
919
1031
 
1032
+ /** The displays as an AIR application's Screen reads them: the work area is past the system's bars. */
1033
+ function screenStates(): AirScreenState[] {
1034
+ const primary = screen.getPrimaryDisplay().id;
1035
+ return screen.getAllDisplays().map((display) => ({
1036
+ bounds: display.bounds,
1037
+ visibleBounds: display.workArea,
1038
+ colorDepth: display.colorDepth,
1039
+ main: display.id === primary,
1040
+ }));
1041
+ }
1042
+
920
1043
  /**
921
1044
  * An AIR application's reach, shared/api.ts DesktopAir: the file system
922
1045
  * and the window, only while one the user opened plays and only for its
@@ -990,7 +1113,7 @@ function listenForAir(): void {
990
1113
  }
991
1114
 
992
1115
  try {
993
- const real = await realpath(path);
1116
+ const real = await realpath(resolveCase(path));
994
1117
  return sandbox.startApplication(real) ? new Uint8Array(await readFile(real)) : null;
995
1118
  } catch {
996
1119
  return null;
@@ -999,6 +1122,9 @@ function listenForAir(): void {
999
1122
  ipcMain.on("air:window", (event) => {
1000
1123
  event.returnValue = playing(event) && window ? windowState(window) : null;
1001
1124
  });
1125
+ ipcMain.on("air:screens", (event) => {
1126
+ event.returnValue = playing(event) ? screenStates() : [];
1127
+ });
1002
1128
  ipcMain.on("air:set-bounds", (event, bounds: unknown) => {
1003
1129
  const b = bounds as { x?: unknown; y?: unknown; width?: unknown; height?: unknown } | null;
1004
1130
  const n = (v: unknown) => (typeof v === "number" && Number.isFinite(v) ? Math.round(v) : null);
@@ -1068,7 +1194,7 @@ async function fetchForPage(
1068
1194
  request: unknown,
1069
1195
  deliver: (response: NetworkResponse) => void,
1070
1196
  ): Promise<NetworkResponse> {
1071
- const asked = request as { url?: unknown; purpose?: unknown } | null;
1197
+ const asked = request as { url?: unknown; purpose?: unknown; method?: unknown } | null;
1072
1198
  const url = typeof asked?.url === "string" ? asked.url : "";
1073
1199
  const shown = url.slice(0, 2048);
1074
1200
  const movie = asked?.purpose === "movie" && sandbox.startRemote(url);
@@ -1079,13 +1205,27 @@ async function fetchForPage(
1079
1205
 
1080
1206
  const controller = new AbortController();
1081
1207
  requests.set(id, controller);
1208
+ const started = performance.now();
1209
+ const method = typeof asked?.method === "string" ? asked.method.slice(0, 16) : "GET";
1082
1210
  try {
1083
- return await network.fetch(request, controller.signal, deliver);
1211
+ const response = await network.fetch(request, controller.signal, deliver);
1212
+ netLog?.(
1213
+ requestLine(
1214
+ method,
1215
+ url,
1216
+ response.status,
1217
+ response.bytes?.length ?? null,
1218
+ performance.now() - started,
1219
+ ) + via,
1220
+ );
1221
+ return response;
1084
1222
  } catch (error) {
1085
1223
  // A refusal says why; anything else, which should not happen, is said too.
1086
1224
  if (!controller.signal.aborted) {
1087
1225
  const why = error instanceof Refused ? error.message : `failed: ${error}`;
1088
1226
  process.stderr.write(`swf2es: not loading ${shown}: ${why}\n`);
1227
+ } else {
1228
+ netLog?.(failureLine(method, url, "aborted", performance.now() - started) + via);
1089
1229
  }
1090
1230
 
1091
1231
  throw error;
@@ -1099,10 +1239,25 @@ async function fetchForPage(
1099
1239
  // One app to a user data directory: a second start hands its SWF to the first and quits.
1100
1240
  const first = app.requestSingleInstanceLock();
1101
1241
  if (!first) {
1242
+ if (proxied) {
1243
+ process.stderr.write(
1244
+ "swf2es: handed to the app already open, which opens it only if started with the same --proxy\n",
1245
+ );
1246
+ }
1247
+
1102
1248
  app.quit();
1103
1249
  }
1104
1250
 
1105
1251
  app.on("second-instance", (_event, argv, workingDirectory) => {
1252
+ // What it names would go through another proxy than this app's, or past it.
1253
+ const asked = proxyArgument(argv.slice(1));
1254
+ if (asked && !("proxy" in asked && sameProxy(asked.proxy, proxy))) {
1255
+ process.stderr.write(
1256
+ "swf2es: not opening what a second start named: its --proxy is not this app's\n",
1257
+ );
1258
+ return;
1259
+ }
1260
+
1106
1261
  if (window) {
1107
1262
  if (window.isMinimized()) {
1108
1263
  window.restore();
@@ -1152,6 +1307,15 @@ void app.whenReady().then(() => {
1152
1307
  }
1153
1308
 
1154
1309
  settings = new SettingsFile(app.getPath("userData"));
1310
+ // A playerglobal chosen once is kept across versions of the app: said at each start, as a
1311
+ // library older or newer than the app's own makes SWFs fail in ways nothing else explains.
1312
+ const chosen = settings.libraries().playerglobal;
1313
+ if (chosen) {
1314
+ process.stderr.write(
1315
+ `swf2es: playing on the playerglobal chosen in File, ${chosen}, not the app's own\n`,
1316
+ );
1317
+ }
1318
+
1155
1319
  serve(session.defaultSession, sandbox, () => settings.libraries());
1156
1320
  // The page never goes to the network itself: a SWF's requests go through
1157
1321
  // the main process (network.ts), which sends them without this session.
@@ -0,0 +1,108 @@
1
+ // --log-network's lines: what a SWF asked of the network, as the terminal
2
+ // shows it. The page has no network of its own, so its DevTools' Network
3
+ // tab shows none of this; the main process, which sends the requests, says
4
+ // them here instead. Apart from Electron, so that node tests it.
5
+
6
+ /** A size as a person reads it: bytes, then kB and MB to one decimal. */
7
+ function size(bytes: number): string {
8
+ if (bytes < 1024) {
9
+ return `${bytes} B`;
10
+ }
11
+
12
+ return bytes < 1024 * 1024
13
+ ? `${(bytes / 1024).toFixed(1)} kB`
14
+ : `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
15
+ }
16
+
17
+ /** A URL short enough for a line, and with nothing a terminal would act on. */
18
+ function shown(url: string): string {
19
+ // biome-ignore lint/suspicious/noControlCharactersInRegex: control characters are what is taken out
20
+ return url.slice(0, 2048).replace(/[\u0000-\u001f\u007f]/g, "?");
21
+ }
22
+
23
+ /** A request answered: its method, URL, status, body's size and how long it took. */
24
+ export function requestLine(
25
+ method: string,
26
+ url: string,
27
+ status: number,
28
+ bytes: number | null,
29
+ ms: number,
30
+ ): string {
31
+ const body = bytes === null ? "no body" : size(bytes);
32
+ return `swf2es: net ${method} ${shown(url)} ${status} ${body} ${Math.round(ms)} ms`;
33
+ }
34
+
35
+ /** A request that got no answer: refused, failed or aborted, and why. */
36
+ export function failureLine(method: string, url: string, why: string, ms: number): string {
37
+ return `swf2es: net ${method} ${shown(url)} ${why} ${Math.round(ms)} ms`;
38
+ }
39
+
40
+ /** A socket's connection asked for, opened, refused or closed. */
41
+ export function socketLine(id: number, what: string): string {
42
+ return `swf2es: socket #${id} ${shown(what)}`;
43
+ }
44
+
45
+ /** How much of a message --log-sockets shows. */
46
+ const PREVIEW = 512;
47
+
48
+ /** The start of `bytes`: as text where it is text, bytes that are no printable ASCII as dots, else in hex. */
49
+ function preview(bytes: Uint8Array): string {
50
+ const shownBytes = bytes.subarray(0, PREVIEW);
51
+ let printable = 0;
52
+ for (let i = 0; i < shownBytes.length; i++) {
53
+ const b = shownBytes[i];
54
+ if ((b >= 32 && b < 127) || b === 9 || b === 10 || b === 13 || b === 0) {
55
+ printable++;
56
+ }
57
+ }
58
+
59
+ let text = "";
60
+ // Mostly text, as a line protocol's messages are, a NUL ending each: shown as text.
61
+ if (printable * 10 >= shownBytes.length * 9) {
62
+ for (let i = 0; i < shownBytes.length; i++) {
63
+ const b = shownBytes[i];
64
+ text += b >= 32 && b < 127 ? String.fromCharCode(b) : ".";
65
+ }
66
+ } else {
67
+ for (let i = 0; i < shownBytes.length; i++) {
68
+ text += (i > 0 ? " " : "") + shownBytes[i].toString(16).padStart(2, "0");
69
+ }
70
+ }
71
+
72
+ return text + (bytes.length > PREVIEW ? "…" : "");
73
+ }
74
+
75
+ /**
76
+ * What a socket sent (">") or received ("<"): its size and its start, as
77
+ * text where it is text, bytes that are no printable ASCII as dots, else
78
+ * in hex. A message past PREVIEW bytes ends in an ellipsis.
79
+ */
80
+ export function socketDataLine(id: number, sent: boolean, bytes: Uint8Array): string {
81
+ return `swf2es: socket #${id} ${sent ? ">" : "<"} ${bytes.length} B ${preview(bytes)}`;
82
+ }
83
+
84
+ /** A WebSocket's connection asked for, opened, refused, failed or closed. */
85
+ export function webSocketLine(id: number, what: string): string {
86
+ return `swf2es: websocket #${id} ${shown(what)}`;
87
+ }
88
+
89
+ /**
90
+ * What a WebSocket sent (">") or received ("<"): a text message's size
91
+ * in UTF-8 and its first PREVIEW characters, control characters as dots;
92
+ * a binary one's as a socket's.
93
+ */
94
+ export function webSocketDataLine(id: number, sent: boolean, data: string | Uint8Array): string {
95
+ const head = `swf2es: websocket #${id} ${sent ? ">" : "<"}`;
96
+ if (typeof data !== "string") {
97
+ return `${head} binary ${data.length} B ${preview(data)}`;
98
+ }
99
+
100
+ // By code point, so that no pair is cut in two.
101
+ const characters = Array.from(data.slice(0, PREVIEW * 2)).slice(0, PREVIEW);
102
+ const text = characters
103
+ .join("")
104
+ // biome-ignore lint/suspicious/noControlCharactersInRegex: control characters are what is taken out
105
+ .replace(/[\u0000-\u001f\u007f-\u009f\u2028\u2029]/g, ".");
106
+ const more = text.length < data.length ? "…" : "";
107
+ return `${head} text ${Buffer.byteLength(data)} B ${text}${more}`;
108
+ }