@amxts/core 0.2.3 → 0.3.0

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 (71) hide show
  1. package/README.md +5 -5
  2. package/README.ru.md +5 -5
  3. package/as/amxts.d.ts +13 -3
  4. package/as/entities.ts +571 -501
  5. package/as/events.ts +3323 -557
  6. package/as/facade.ts +3079 -718
  7. package/as/fetch.ts +2 -2
  8. package/as/fs.ts +352 -59
  9. package/as/hlds.ts +121 -120
  10. package/as/hooks.ts +10585 -5177
  11. package/as/lib/check.ts +3 -2
  12. package/as/natives.ts +856 -1272
  13. package/as/promise.ts +37 -27
  14. package/as/vector.ts +94 -14
  15. package/includes/order.txt +17 -19
  16. package/node_modules/assemblyscript/std/assembly/index.d.ts +1 -1
  17. package/package.json +9 -6
  18. package/runtime/deps/assemblyscript/dist/asc.js +136 -42
  19. package/runtime/deps/assemblyscript/dist/assemblyscript.js +141 -141
  20. package/runtime/licenses/README.md +9 -7
  21. package/runtime/licenses/ReGameDLL-LICENSE +21 -0
  22. package/runtime/licenses/ReHLDS-LICENSE +21 -0
  23. package/runtime/licenses/zlib-LICENSE +22 -0
  24. package/runtime/natives.txt +618 -523
  25. package/runtime/patches/README.md +87 -1
  26. package/runtime/patches/assemblyscript-0.28.20-amxts.patch +4838 -416
  27. package/runtime/patches/wamr-2.4.5-amxts.patch +417 -5
  28. package/scripts/auto-imports.ts +1 -1
  29. package/scripts/build-identity.ts +102 -65
  30. package/scripts/build-wasm.ts +17 -13
  31. package/scripts/check.ts +4 -1
  32. package/scripts/client-messages.ts +73 -1
  33. package/scripts/compile-cache.ts +11 -8
  34. package/scripts/compile-pool.ts +6 -3
  35. package/scripts/compile-worker.ts +23 -23
  36. package/scripts/compile.ts +142 -19
  37. package/scripts/docs/as/amxts.ts +10 -4
  38. package/scripts/docs/as/facade.ts +1779 -275
  39. package/scripts/docs/as/fetch.ts +2 -2
  40. package/scripts/docs/as/fs.ts +190 -6
  41. package/scripts/docs/as/vector.ts +81 -0
  42. package/scripts/docs/entities.ts +12 -0
  43. package/scripts/docs/events.ts +49 -13
  44. package/scripts/docs/game.ts +6 -0
  45. package/scripts/docs/messages.ts +10 -1
  46. package/scripts/hlds-events.ts +11 -11
  47. package/scripts/includes.ts +3 -4
  48. package/scripts/leaf-natives.ts +84 -0
  49. package/scripts/player-fields.ts +1 -1
  50. package/scripts/plugin-natives.ts +1 -1
  51. package/scripts/prebuilt.ts +42 -11
  52. package/scripts/project.ts +63 -4
  53. package/scripts/reapi-events.ts +9 -9
  54. package/scripts/server-env.ts +145 -0
  55. package/scripts/shared-modules.ts +23 -7
  56. package/scripts/system.ts +1 -1
  57. package/scripts/tracked-fs.ts +11 -2
  58. package/scripts/typed-commands.ts +153 -15
  59. package/scripts/upgrade-calls.ts +225 -0
  60. package/scripts/upgrade-names.ts +1 -4
  61. package/scripts/upgrade.ts +47 -34
  62. package/src/cli-api.mjs +3 -2
  63. package/src/system.mjs +21 -11
  64. package/src/testing/compile.ts +31 -8
  65. package/src/testing/coroutines.ts +0 -10
  66. package/src/testing/memory.ts +89 -2
  67. package/src/testing/natives.ts +67 -30
  68. package/src/testing/server.ts +1094 -155
  69. package/src/testing/tables.ts +23 -6
  70. package/src/typecheck.mjs +110 -0
  71. package/runtime/licenses/Bun-LICENSE.md +0 -84
package/as/fetch.ts CHANGED
@@ -525,7 +525,7 @@ export class URLSearchParams {
525
525
  const order: i32[] = [];
526
526
  for (let i: i32 = 0; i < this.__names.length; i++) order.push(i);
527
527
  const names = this.__names;
528
- order.sort((a: i32, b: i32): i32 => (names[a] < names[b] ? -1 : names[a] > names[b] ? 1 : a - b));
528
+ order.sort((a: i32, b: i32) => (names[a] < names[b] ? -1 : names[a] > names[b] ? 1 : a - b));
529
529
  this.__names = order.map<string>((i: i32): string => names[i]);
530
530
  const values = this.__values;
531
531
  this.__values = order.map<string>((i: i32): string => values[i]);
@@ -1440,7 +1440,7 @@ function withQuery(url: string, query: Record<string, string> | undefined): stri
1440
1440
  *
1441
1441
  * const { data, error } = await useFetch<Weather>("https://example.com/weather", { query: { city: "Paris" } });
1442
1442
  * if (error) return console.error(error.message);
1443
- * print(player, `${data!.temperature} °C`);
1443
+ * player.print(`${data!.temperature} °C`);
1444
1444
  * ```
1445
1445
  *
1446
1446
  * A body that is an object goes as JSON: `useFetch<Answer, Report>(url, { method: "POST", body: report })`.
package/as/fs.ts CHANGED
@@ -12,7 +12,10 @@
12
12
  //
13
13
  // Text is UTF-8 both ways. A file is read whole, whatever its size: 128 bytes
14
14
  // a native call (the thunk's buffer for an array it does not know the size
15
- // of), never through a 255-character line buffer.
15
+ // of), never through a 255-character line buffer. Bytes go through the
16
+ // module's own fs_read and fs_write, a whole file a call, and refuse a path
17
+ // that leaves the game folder; extract reads the archive here and inflates
18
+ // with the module's zlib (zip_inflate).
16
19
  //
17
20
  // There are no exceptions in AssemblyScript, so the synchronous functions say
18
21
  // they failed by their result - null, false - and the promise ones reject
@@ -23,75 +26,91 @@
23
26
 
24
27
  // Promise, for the promise versions, whether or not the plugin imports the facade.
25
28
  import "./promise";
29
+ import { __textFrom, __textInto, __textRoom } from "./natives";
26
30
 
27
31
  // @ts-ignore: decorator
28
- @external("env", "fopen") declare function _fopen(name: usize, mode: usize, valve: i32, pathId: usize): i32;
32
+ @external("env", "fopen") declare function _fopen(name: string, mode: string, valve: i32, pathId: usize): i32;
29
33
  // @ts-ignore: decorator
30
34
  @external("env", "fclose") declare function _fclose(file: i32): i32;
31
35
  // @ts-ignore: decorator
32
36
  @external("env", "fread_blocks") declare function _freadBlocks(file: i32, data: usize, blocks: i32, mode: i32): i32;
33
37
  // @ts-ignore: decorator
34
- @external("env", "fputs") declare function _fputs(file: i32, text: usize, nullTerm: i32): i32;
38
+ @external("env", "fputs") declare function _fputs(file: i32, text: string, nullTerm: i32): i32;
35
39
  // @ts-ignore: decorator
36
- @external("env", "file_exists") declare function _fileExists(name: usize, valve: i32): i32;
40
+ @external("env", "file_exists") declare function _fileExists(name: string, valve: i32): i32;
37
41
  // @ts-ignore: decorator
38
- @external("env", "dir_exists") declare function _dirExists(name: usize, valve: i32): i32;
42
+ @external("env", "dir_exists") declare function _dirExists(name: string, valve: i32): i32;
39
43
  // @ts-ignore: decorator
40
- @external("env", "open_dir") declare function _openDir(dir: usize, first: usize, length: i32, type: usize, valve: i32, pathId: usize): i32;
44
+ @external("env", "open_dir") declare function _openDir(dir: string, first: usize, length: i32, type: usize, valve: i32, pathId: usize): i32;
41
45
  // @ts-ignore: decorator
42
46
  @external("env", "next_file") declare function _nextFile(handle: i32, name: usize, length: i32, type: usize): i32;
43
47
  // @ts-ignore: decorator
44
48
  @external("env", "close_dir") declare function _closeDir(handle: i32): i32;
45
49
  // @ts-ignore: decorator
46
- @external("env", "mkdir") declare function _mkdir(name: usize, mode: i32, valve: i32, pathId: usize): i32;
50
+ @external("env", "mkdir") declare function _mkdir(name: string, mode: i32, valve: i32, pathId: string): i32;
51
+ // @ts-ignore: decorator
52
+ @external("env", "delete_file") declare function _deleteFile(name: string, valve: i32, pathId: string): i32;
53
+ // @ts-ignore: decorator
54
+ @external("env", "rename_file") declare function _renameFile(from: string, to: string, relative: i32): i32;
55
+ // @ts-ignore: decorator
56
+ @external("env", "rmdir") declare function _rmdir(name: string): i32;
57
+ // @ts-ignore: decorator
58
+ @external("env", "file_size") declare function _fileSize(name: string, flag: i32, valve: i32, pathId: string): i32;
59
+ // @ts-ignore: decorator
60
+ @external("env", "GetFileTime") declare function _fileTime(name: string, flag: i32): i32;
61
+ // @ts-ignore: decorator
62
+ @external("env", "fs_read") declare function _readBytes(name: string, out: usize, max: i32): i32;
63
+ // @ts-ignore: decorator
64
+ @external("env", "fs_write") declare function _writeBytes(name: string, data: usize, length: i32, append: i32): i32;
65
+ // @ts-ignore: decorator
66
+ @external("env", "zip_inflate") declare function _inflate(data: usize, length: i32, out: usize, max: i32): i32;
47
67
 
48
68
  // fread_blocks' mode for one byte per cell.
49
69
  const BLOCK_CHAR: i32 = 1;
50
70
  // What one fread_blocks call reads: the module's thunk copies exactly this
51
71
  // many cells of an array whose size it cannot know.
52
72
  const READ_CELLS: i32 = 128;
53
- // What one fputs call writes: the thunk copies a string up to 511 cells.
54
- const WRITE_BYTES: i32 = 500;
55
- // A name from open_dir / next_file.
56
- const NAME_CELLS: i32 = 256;
73
+ // What one fputs call writes: AMX Mod X reads 16383 bytes of a string, and a
74
+ // UTF-16 unit is three bytes at most.
75
+ const WRITE_UNITS: i32 = 4096;
76
+ // A name from open_dir / next_file, which the module writes as UTF-8 bytes.
77
+ const NAME_BYTES: i32 = 256;
57
78
  // mkdir's FPERM_DIR_DEFAULT: rwxrwxr-x, where the system has permissions.
58
79
  const DIR_MODE: i32 = 0o775;
80
+ // rename_file's flag for names relative to the game folder, as every other path here is.
81
+ const GAME_RELATIVE: i32 = 1;
82
+ // GetFileTime's FileTime_LastChange.
83
+ const LAST_CHANGE: i32 = 2;
59
84
 
60
- /** Text as a Pawn string: its UTF-8 bytes, a byte a cell, and a zero cell. */
61
- function cells(text: string): StaticArray<i32> {
62
- const bytes = Uint8Array.wrap(String.UTF8.encode(text));
63
- const out = new StaticArray<i32>(bytes.length + 1);
64
- for (let i: i32 = 0; i < bytes.length; i++) unchecked(out[i] = <i32>unchecked(bytes[i]));
65
- return out;
85
+ function open(path: string, mode: string): i32 {
86
+ return _fopen(path, mode, 0, 0);
66
87
  }
67
88
 
68
- /** A Pawn string of UTF-8 bytes back as text. */
69
- function textOf(from: StaticArray<i32>): string {
70
- const bytes = new Array<u8>();
71
- for (let i: i32 = 0; i < from.length; i++) {
72
- const c = unchecked(from[i]);
73
- if (c == 0) break;
74
- bytes.push(<u8>c);
89
+ /** Writes text, or the bytes of a `Uint8Array` or an `ArrayBuffer`; `append` adds them at the end. */
90
+ function write<T>(path: string, data: T, append: bool): bool {
91
+ if (isString<T>()) return writeText(path, changetype<string>(data), append ? "ab" : "wb");
92
+ if (idof<T>() == idof<Uint8Array>()) {
93
+ const bytes = changetype<Uint8Array>(data);
94
+ return _writeBytes(path, bytes.dataStart, bytes.length, append ? 1 : 0) != 0;
75
95
  }
76
- return String.UTF8.decodeUnsafe(bytes.dataStart, <usize>bytes.length);
77
- }
78
-
79
- function open(path: string, mode: string): i32 {
80
- return _fopen(changetype<usize>(cells(path)), changetype<usize>(cells(mode)), 0, 0);
96
+ if (idof<T>() == idof<ArrayBuffer>()) {
97
+ const buffer = changetype<ArrayBuffer>(data);
98
+ return _writeBytes(path, changetype<usize>(buffer), buffer.byteLength, append ? 1 : 0) != 0;
99
+ }
100
+ ERROR("a file takes text, a Uint8Array or an ArrayBuffer");
101
+ return false;
81
102
  }
82
103
 
83
- function write(path: string, data: string, mode: string): bool {
104
+ function writeText(path: string, data: string, mode: string): bool {
84
105
  const file = open(path, mode);
85
106
  if (file == 0) return false;
86
107
 
87
- const bytes = Uint8Array.wrap(String.UTF8.encode(data));
88
- const chunk = new StaticArray<i32>(WRITE_BYTES + 1);
89
-
90
- for (let at: i32 = 0; at < bytes.length; at += WRITE_BYTES) {
91
- const count = min(WRITE_BYTES, bytes.length - at);
92
- for (let i: i32 = 0; i < count; i++) unchecked(chunk[i] = <i32>unchecked(bytes[at + i]));
93
- unchecked(chunk[count] = 0);
94
- _fputs(file, changetype<usize>(chunk), 0);
108
+ for (let at: i32 = 0; at < data.length;) {
109
+ let end = min(at + WRITE_UNITS, data.length);
110
+ // A surrogate pair is one letter: it goes in one piece.
111
+ if (end < data.length && (data.charCodeAt(end - 1) & 0xFC00) == 0xD800) end--;
112
+ _fputs(file, data.substring(at, end), 0);
113
+ at = end;
95
114
  }
96
115
 
97
116
  _fclose(file);
@@ -133,26 +152,51 @@ export function readFileSync(path: string): string | null {
133
152
  }
134
153
 
135
154
  /**
136
- * Writes text to a file, replacing what was there; `false` when it cannot be
137
- * opened (a folder that does not exist, for one).
155
+ * Writes text, or bytes - a `Uint8Array` or an `ArrayBuffer` - to a file,
156
+ * replacing what was there; `false` when it cannot be opened (a folder that
157
+ * does not exist, for one). Bytes are not written past the game folder.
138
158
  *
139
159
  * ```ts
140
160
  * fs.writeFileSync("addons/amxmodx/data/last-map.txt", server.map);
161
+ * fs.writeFileSync("maps/kz_map.bsp", await response.arrayBuffer());
141
162
  * ```
142
163
  *
143
164
  * Pawn: `fopen`, `fputs`
144
165
  */
145
- export function writeFileSync(path: string, data: string): boolean {
146
- return write(path, data, "wb");
166
+ export function writeFileSync<T extends string | Uint8Array | ArrayBuffer = string>(path: string, data: T): boolean {
167
+ return write<T>(path, data, false);
147
168
  }
148
169
 
149
170
  /**
150
- * Adds text to the end of a file, making it if there is none; `false` when it cannot be opened.
171
+ * Adds text or bytes to the end of a file, making it if there is none; `false` when it cannot be opened.
151
172
  *
152
173
  * Pawn: `fopen(path, "a")`, `fputs`
153
174
  */
154
- export function appendFileSync(path: string, data: string): boolean {
155
- return write(path, data, "ab");
175
+ export function appendFileSync<T extends string | Uint8Array | ArrayBuffer = string>(path: string, data: T): boolean {
176
+ return write<T>(path, data, true);
177
+ }
178
+
179
+ /**
180
+ * Reads a whole file as bytes; `null` when it cannot be opened or the path
181
+ * leaves the game folder.
182
+ */
183
+ export function readBytesSync(path: string): Uint8Array | null {
184
+ let size = _readBytes(path, 0, 0);
185
+ // A file that grows between the two calls is read again at its new size.
186
+ while (size >= 0) {
187
+ const bytes = new Uint8Array(size);
188
+ const read = _readBytes(path, bytes.dataStart, size);
189
+ if (read <= size) return read == size ? bytes : bytes.slice(0, max(read, 0));
190
+ size = read;
191
+ }
192
+ return null;
193
+ }
194
+
195
+ /** `readBytesSync` as a promise, rejected when the file cannot be opened. */
196
+ export function readBytes(path: string): Promise<Uint8Array> {
197
+ const bytes = readBytesSync(path);
198
+ if (bytes == null) return Promise.reject<Uint8Array>(missing(path, "open"));
199
+ return Promise.resolve<Uint8Array>(bytes);
156
200
  }
157
201
 
158
202
  /**
@@ -161,8 +205,7 @@ export function appendFileSync(path: string, data: string): boolean {
161
205
  * Pawn: `file_exists`, `dir_exists`
162
206
  */
163
207
  export function existsSync(path: string): boolean {
164
- const name = changetype<usize>(cells(path));
165
- return _fileExists(name, 0) != 0 || _dirExists(name, 0) != 0;
208
+ return _fileExists(path, 0) != 0 || _dirExists(path, 0) != 0;
166
209
  }
167
210
 
168
211
  /**
@@ -176,16 +219,16 @@ export function existsSync(path: string): boolean {
176
219
  * Pawn: `open_dir`, `next_file`
177
220
  */
178
221
  export function readdirSync(path: string): string[] | null {
179
- const name = new StaticArray<i32>(NAME_CELLS);
222
+ const name = __textRoom(NAME_BYTES);
180
223
  const type = new StaticArray<i32>(1);
181
- const handle = _openDir(changetype<usize>(cells(path)), changetype<usize>(name), NAME_CELLS - 1, changetype<usize>(type), 0, 0);
224
+ const handle = _openDir(path, <usize>__textInto(name), NAME_BYTES - 1, changetype<usize>(type), 0, 0);
182
225
  if (handle == 0) return null;
183
226
 
184
227
  const names: string[] = [];
185
228
  for (;;) {
186
- const entry = textOf(name);
229
+ const entry = __textFrom(name);
187
230
  if (entry != "." && entry != "..") names.push(entry);
188
- if (_nextFile(handle, changetype<usize>(name), NAME_CELLS - 1, changetype<usize>(type)) == 0) break;
231
+ if (_nextFile(handle, <usize>__textInto(name), NAME_BYTES - 1, changetype<usize>(type)) == 0) break;
189
232
  }
190
233
 
191
234
  _closeDir(handle);
@@ -228,11 +271,11 @@ export function mkdirSync(path: string, options: MakeDirectoryOptions = new Make
228
271
  }
229
272
 
230
273
  function isFolder(path: string): bool {
231
- return _dirExists(changetype<usize>(cells(path)), 0) != 0;
274
+ return _dirExists(path, 0) != 0;
232
275
  }
233
276
 
234
277
  function makeOne(path: string): bool {
235
- return _mkdir(changetype<usize>(cells(path)), DIR_MODE, 0, changetype<usize>(cells("GAMECONFIG"))) == 0;
278
+ return _mkdir(path, DIR_MODE, 0, "GAMECONFIG") == 0;
236
279
  }
237
280
 
238
281
  /** `mkdirSync` as a promise, rejected when the folder cannot be made. */
@@ -258,13 +301,13 @@ export function readFile(path: string): Promise<string> {
258
301
  }
259
302
 
260
303
  /** `writeFileSync` as a promise, rejected when the file cannot be opened. */
261
- export function writeFile(path: string, data: string): Promise<void> {
262
- return settled(writeFileSync(path, data), path);
304
+ export function writeFile<T extends string | Uint8Array | ArrayBuffer = string>(path: string, data: T): Promise<void> {
305
+ return done(write<T>(path, data, false), path, "open");
263
306
  }
264
307
 
265
308
  /** `appendFileSync` as a promise, rejected when the file cannot be opened. */
266
- export function appendFile(path: string, data: string): Promise<void> {
267
- return settled(appendFileSync(path, data), path);
309
+ export function appendFile<T extends string | Uint8Array | ArrayBuffer = string>(path: string, data: T): Promise<void> {
310
+ return done(write<T>(path, data, true), path, "open");
268
311
  }
269
312
 
270
313
  /** `existsSync` as a promise. */
@@ -279,9 +322,259 @@ export function readdir(path: string): Promise<string[]> {
279
322
  return Promise.resolve<string[]>(names);
280
323
  }
281
324
 
282
- function settled(ok: bool, path: string): Promise<void> {
325
+ /**
326
+ * Deletes a file; `false` when there is none or it cannot be deleted.
327
+ *
328
+ * Pawn: `delete_file`, `unlink`
329
+ */
330
+ export function unlinkSync(path: string): boolean {
331
+ return _deleteFile(path, 0, "GAMECONFIG") != 0;
332
+ }
333
+
334
+ /**
335
+ * Moves or renames a file: `fs.renameSync("addons/amxmodx/data/top.txt",
336
+ * "addons/amxmodx/data/top.old")`; `false` when it cannot.
337
+ *
338
+ * Pawn: `rename_file`
339
+ */
340
+ export function renameSync(from: string, to: string): boolean {
341
+ return _renameFile(from, to, GAME_RELATIVE) != 0;
342
+ }
343
+
344
+ /**
345
+ * Deletes an empty folder; `false` when there is none, it holds something or
346
+ * it cannot be deleted.
347
+ *
348
+ * Pawn: `rmdir`
349
+ */
350
+ export function rmdirSync(path: string): boolean {
351
+ return _rmdir(path) != 0;
352
+ }
353
+
354
+ /** A file's or a folder's facts from `statSync`, as Node's `fs.Stats`. */
355
+ export class Stats {
356
+ constructor(
357
+ /** The size in bytes; `0` for a folder. */
358
+ readonly size: number,
359
+ /** The moment it last changed. */
360
+ readonly mtime: Date,
361
+ private readonly folder: bool,
362
+ ) {}
363
+
364
+ /** Whether it is a file. */
365
+ isFile(): boolean {
366
+ return !this.folder;
367
+ }
368
+
369
+ /** Whether it is a folder. */
370
+ isDirectory(): boolean {
371
+ return this.folder;
372
+ }
373
+ }
374
+
375
+ /**
376
+ * A file's or a folder's size and the moment it last changed; `null` when
377
+ * there is none: `fs.statSync("addons/amxmodx/logs/error.log")?.size`.
378
+ *
379
+ * Pawn: `file_size`, `GetFileTime`
380
+ */
381
+ export function statSync(path: string): Stats | null {
382
+ const folder = _dirExists(path, 0) != 0;
383
+ if (!folder && _fileExists(path, 0) == 0) return null;
384
+ const size = folder ? 0 : _fileSize(path, 0, 0, "*");
385
+ return new Stats(size, new Date(<i64>_fileTime(path, LAST_CHANGE) * 1000), folder);
386
+ }
387
+
388
+ /** `unlinkSync` as a promise, rejected when the file cannot be deleted. */
389
+ export function unlink(path: string): Promise<void> {
390
+ return done(unlinkSync(path), path, "unlink");
391
+ }
392
+
393
+ /** `renameSync` as a promise, rejected when the file cannot be moved. */
394
+ export function rename(from: string, to: string): Promise<void> {
395
+ return done(renameSync(from, to), from, "rename");
396
+ }
397
+
398
+ /** `rmdirSync` as a promise, rejected when the folder cannot be deleted. */
399
+ export function rmdir(path: string): Promise<void> {
400
+ return done(rmdirSync(path), path, "rmdir");
401
+ }
402
+
403
+ /** `statSync` as a promise, rejected when there is no such file or folder. */
404
+ export function stat(path: string): Promise<Stats> {
405
+ const stats = statSync(path);
406
+ if (stats == null) return Promise.reject<Stats>(missing(path, "stat"));
407
+ return Promise.resolve<Stats>(stats);
408
+ }
409
+
410
+ function done(ok: bool, path: string, call: string): Promise<void> {
283
411
  const promise = __co_promise<void>();
284
412
  if (ok) promise.__fulfillVoid();
285
- else promise.__reject(missing(path, "open"));
413
+ else promise.__reject(missing(path, call));
286
414
  return promise;
287
415
  }
416
+
417
+ // ---------------------------------------------------------------- zip
418
+
419
+ // The records of a zip archive: the end of its central directory, an entry
420
+ // of the directory, an entry's local header.
421
+ const ZIP_END: u32 = 0x06054B50;
422
+ const ZIP_ENTRY: u32 = 0x02014B50;
423
+ const ZIP_LOCAL: u32 = 0x04034B50;
424
+ // The end record is 22 bytes and its comment 65535 at most.
425
+ const ZIP_END_SIZE: i32 = 22;
426
+ const ZIP_COMMENT_MAX: i32 = 0xFFFF;
427
+ const ZIP_STORED: i32 = 0;
428
+ const ZIP_DEFLATED: i32 = 8;
429
+ // An entry's flags: encrypted.
430
+ const ZIP_ENCRYPTED: i32 = 1;
431
+
432
+ /** A file of an archive, from `extract`: its path inside the archive and its bytes. */
433
+ export class ArchiveEntry {
434
+ constructor(
435
+ /** The path inside the archive, `/` between folders: `"maps/de_dust2.bsp"`. */
436
+ readonly path: string,
437
+ /** The file's bytes. */
438
+ readonly data: Uint8Array,
439
+ ) {}
440
+ }
441
+
442
+ /**
443
+ * The files of an archive, with their paths inside it; folders are not
444
+ * listed. The archive's kind is read off its first bytes: a zip archive,
445
+ * stored or deflated. Throws an `Error` for a kind it does not read - naming
446
+ * the ones it does - an entry whose bytes do not check out, and a path that
447
+ * would leave the folder it is unpacked into.
448
+ *
449
+ * ```ts
450
+ * for (const entry of fs.extract(await response.arrayBuffer())) {
451
+ * fs.writeFileSync(entry.path, entry.data);
452
+ * }
453
+ * ```
454
+ */
455
+ export function extract<T extends Uint8Array | ArrayBuffer>(archive: T): ArchiveEntry[] {
456
+ if (idof<T>() == idof<Uint8Array>()) return extractBytes(changetype<Uint8Array>(archive));
457
+ if (idof<T>() == idof<ArrayBuffer>()) return extractBytes(Uint8Array.wrap(changetype<ArrayBuffer>(archive)));
458
+ ERROR("extract takes a Uint8Array or an ArrayBuffer");
459
+ return [];
460
+ }
461
+
462
+ // The archives extract reads, for its errors to name.
463
+ const READS = "zip";
464
+
465
+ function extractBytes(data: Uint8Array): ArchiveEntry[] {
466
+ const kind = archiveKind(data);
467
+ if (kind == "zip") return unzip(data);
468
+ if (kind.length > 0) throw new Error(`extract: a ${kind} archive is not read yet - extract reads ${READS}`);
469
+ throw new Error(`extract: this is not an archive extract reads - it reads ${READS}`);
470
+ }
471
+
472
+ /** The archive's kind by its first bytes - `"zip"`, `"rar"`, `"7z"`, `"gzip"` - or `""`. */
473
+ function archiveKind(data: Uint8Array): string {
474
+ if (startsWith(data, [0x50, 0x4B, 0x03, 0x04]) || startsWith(data, [0x50, 0x4B, 0x05, 0x06])) return "zip";
475
+ if (startsWith(data, [0x52, 0x61, 0x72, 0x21, 0x1A, 0x07])) return "rar";
476
+ if (startsWith(data, [0x37, 0x7A, 0xBC, 0xAF, 0x27, 0x1C])) return "7z";
477
+ if (startsWith(data, [0x1F, 0x8B])) return "gzip";
478
+ return "";
479
+ }
480
+
481
+ function startsWith(data: Uint8Array, magic: u8[]): bool {
482
+ if (data.length < magic.length) return false;
483
+ for (let i = 0; i < magic.length; i++) {
484
+ if (data[i] != magic[i]) return false;
485
+ }
486
+ return true;
487
+ }
488
+
489
+ function unzip(zip: Uint8Array): ArchiveEntry[] {
490
+ const end = zipEnd(zip);
491
+ const count = <i32>zipShort(zip, end + 10);
492
+ let at = <i32>zipWord(zip, end + 16);
493
+
494
+ const entries: ArchiveEntry[] = [];
495
+ for (let i: i32 = 0; i < count; i++) {
496
+ if (zipWord(zip, at) != ZIP_ENTRY) throw new Error("extract: the archive's directory is damaged");
497
+ const flags = <i32>zipShort(zip, at + 8);
498
+ const method = <i32>zipShort(zip, at + 10);
499
+ const crc = zipWord(zip, at + 16);
500
+ const packed = <i32>zipWord(zip, at + 20);
501
+ const size = <i32>zipWord(zip, at + 24);
502
+ const nameLength = <i32>zipShort(zip, at + 28);
503
+ const local = <i32>zipWord(zip, at + 42);
504
+ const path = zipText(zip, at + 46, nameLength).replaceAll("\\", "/");
505
+ at += 46 + nameLength + <i32>zipShort(zip, at + 30) + <i32>zipShort(zip, at + 32);
506
+
507
+ if (path.endsWith("/")) continue;
508
+ if (leavesFolder(path)) throw new Error(`extract: "${path}" would leave the folder`);
509
+ if (flags & ZIP_ENCRYPTED) throw new Error(`extract: "${path}" is encrypted`);
510
+ if (method != ZIP_STORED && method != ZIP_DEFLATED) throw new Error(`extract: "${path}" is compressed in a way extract does not read`);
511
+
512
+ if (zipWord(zip, local) != ZIP_LOCAL) throw new Error(`extract: "${path}" is damaged`);
513
+ const start = local + 30 + <i32>zipShort(zip, local + 26) + <i32>zipShort(zip, local + 28);
514
+ zipCheck(zip, start, packed);
515
+
516
+ const data = method == ZIP_STORED ? zip.slice(start, start + packed) : new Uint8Array(size);
517
+ if (method == ZIP_DEFLATED && size > 0 && _inflate(zip.dataStart + <usize>start, packed, data.dataStart, size) != size) {
518
+ throw new Error(`extract: "${path}" is damaged`);
519
+ }
520
+ if (data.length != size || crc32(data) != crc) throw new Error(`extract: "${path}" is damaged`);
521
+ entries.push(new ArchiveEntry(path, data));
522
+ }
523
+ return entries;
524
+ }
525
+
526
+ /** Where the end of the central directory starts: the last of its records, past which only its comment lies. */
527
+ function zipEnd(zip: Uint8Array): i32 {
528
+ const last = zip.length - ZIP_END_SIZE;
529
+ for (let at = last; at >= 0 && at >= last - ZIP_COMMENT_MAX; at--) {
530
+ if (zipWord(zip, at) == ZIP_END) return at;
531
+ }
532
+ throw new Error("extract: this is not a zip archive");
533
+ }
534
+
535
+ /** An absolute path, a drive or a `..` among its parts. */
536
+ function leavesFolder(path: string): bool {
537
+ return path.startsWith("/") || path.includes(":") || path.split("/").includes("..");
538
+ }
539
+
540
+ function zipCheck(zip: Uint8Array, at: i32, length: i32): void {
541
+ if (at < 0 || length < 0 || at > zip.length - length) throw new Error("extract: the archive is cut short");
542
+ }
543
+
544
+ function zipShort(zip: Uint8Array, at: i32): u32 {
545
+ zipCheck(zip, at, 2);
546
+ return <u32>load<u16>(zip.dataStart + <usize>at);
547
+ }
548
+
549
+ function zipWord(zip: Uint8Array, at: i32): u32 {
550
+ zipCheck(zip, at, 4);
551
+ return load<u32>(zip.dataStart + <usize>at);
552
+ }
553
+
554
+ function zipText(zip: Uint8Array, at: i32, length: i32): string {
555
+ zipCheck(zip, at, length);
556
+ return String.UTF8.decodeUnsafe(zip.dataStart + <usize>at, <usize>length);
557
+ }
558
+
559
+ // CRC-32 as zip writes it, a table of 256 made on the first archive.
560
+ let crcTable: StaticArray<u32> | null = null;
561
+
562
+ function crc32(data: Uint8Array): u32 {
563
+ let table = crcTable;
564
+ if (table == null) {
565
+ table = new StaticArray<u32>(256);
566
+ for (let n: u32 = 0; n < 256; n++) {
567
+ let c = n;
568
+ for (let k = 0; k < 8; k++) c = c & 1 ? 0xEDB88320 ^ (c >>> 1) : c >>> 1;
569
+ unchecked(table[n] = c);
570
+ }
571
+ crcTable = table;
572
+ }
573
+
574
+ let crc: u32 = 0xFFFFFFFF;
575
+ const start = data.dataStart;
576
+ for (let i: i32 = 0; i < data.length; i++) {
577
+ crc = unchecked(table[(crc ^ <u32>load<u8>(start + <usize>i)) & 0xFF]) ^ (crc >>> 8);
578
+ }
579
+ return crc ^ 0xFFFFFFFF;
580
+ }