@amxts/core 0.2.4 → 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.
- package/README.md +5 -5
- package/README.ru.md +5 -5
- package/as/amxts.d.ts +13 -3
- package/as/entities.ts +571 -501
- package/as/events.ts +3323 -557
- package/as/facade.ts +3079 -718
- package/as/fetch.ts +2 -2
- package/as/fs.ts +352 -59
- package/as/hlds.ts +121 -120
- package/as/hooks.ts +10585 -5177
- package/as/lib/check.ts +3 -2
- package/as/natives.ts +856 -1272
- package/as/promise.ts +37 -27
- package/as/vector.ts +94 -14
- package/includes/order.txt +17 -19
- package/node_modules/assemblyscript/std/assembly/index.d.ts +1 -1
- package/package.json +9 -6
- package/runtime/deps/assemblyscript/dist/asc.js +136 -42
- package/runtime/deps/assemblyscript/dist/assemblyscript.js +141 -141
- package/runtime/licenses/README.md +9 -7
- package/runtime/licenses/ReGameDLL-LICENSE +21 -0
- package/runtime/licenses/ReHLDS-LICENSE +21 -0
- package/runtime/licenses/zlib-LICENSE +22 -0
- package/runtime/natives.txt +618 -523
- package/runtime/patches/README.md +87 -1
- package/runtime/patches/assemblyscript-0.28.20-amxts.patch +4838 -416
- package/runtime/patches/wamr-2.4.5-amxts.patch +417 -5
- package/scripts/auto-imports.ts +1 -1
- package/scripts/build-identity.ts +102 -65
- package/scripts/build-wasm.ts +12 -12
- package/scripts/check.ts +4 -1
- package/scripts/client-messages.ts +73 -1
- package/scripts/compile-cache.ts +11 -8
- package/scripts/compile-pool.ts +6 -3
- package/scripts/compile-worker.ts +23 -23
- package/scripts/compile.ts +142 -19
- package/scripts/docs/as/amxts.ts +10 -4
- package/scripts/docs/as/facade.ts +1779 -275
- package/scripts/docs/as/fetch.ts +2 -2
- package/scripts/docs/as/fs.ts +190 -6
- package/scripts/docs/as/vector.ts +81 -0
- package/scripts/docs/entities.ts +12 -0
- package/scripts/docs/events.ts +49 -13
- package/scripts/docs/game.ts +6 -0
- package/scripts/docs/messages.ts +10 -1
- package/scripts/hlds-events.ts +11 -11
- package/scripts/includes.ts +3 -4
- package/scripts/leaf-natives.ts +84 -0
- package/scripts/player-fields.ts +1 -1
- package/scripts/plugin-natives.ts +1 -1
- package/scripts/prebuilt.ts +41 -10
- package/scripts/project.ts +2 -3
- package/scripts/reapi-events.ts +9 -9
- package/scripts/server-env.ts +145 -0
- package/scripts/shared-modules.ts +23 -7
- package/scripts/system.ts +1 -1
- package/scripts/tracked-fs.ts +11 -2
- package/scripts/typed-commands.ts +153 -15
- package/scripts/upgrade-calls.ts +225 -0
- package/scripts/upgrade-names.ts +1 -4
- package/scripts/upgrade.ts +26 -11
- package/src/cli-api.mjs +3 -2
- package/src/system.mjs +21 -11
- package/src/testing/compile.ts +31 -8
- package/src/testing/coroutines.ts +0 -10
- package/src/testing/memory.ts +89 -2
- package/src/testing/natives.ts +67 -30
- package/src/testing/server.ts +1094 -155
- package/src/testing/tables.ts +23 -6
- package/src/typecheck.mjs +110 -0
- 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)
|
|
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(
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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
|
-
|
|
61
|
-
|
|
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
|
-
/**
|
|
69
|
-
function
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
const
|
|
73
|
-
|
|
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
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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
|
|
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
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
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
|
|
137
|
-
* opened (a folder that
|
|
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:
|
|
146
|
-
return write(path, data,
|
|
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:
|
|
155
|
-
return write(path, data,
|
|
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
|
-
|
|
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 =
|
|
222
|
+
const name = __textRoom(NAME_BYTES);
|
|
180
223
|
const type = new StaticArray<i32>(1);
|
|
181
|
-
const handle = _openDir(
|
|
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 =
|
|
229
|
+
const entry = __textFrom(name);
|
|
187
230
|
if (entry != "." && entry != "..") names.push(entry);
|
|
188
|
-
if (_nextFile(handle,
|
|
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(
|
|
274
|
+
return _dirExists(path, 0) != 0;
|
|
232
275
|
}
|
|
233
276
|
|
|
234
277
|
function makeOne(path: string): bool {
|
|
235
|
-
return _mkdir(
|
|
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:
|
|
262
|
-
return
|
|
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:
|
|
267
|
-
return
|
|
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
|
-
|
|
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,
|
|
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
|
+
}
|