beam.com 0.1.0-rc.1
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/LICENSE +202 -0
- package/NOTICE +97 -0
- package/README.md +369 -0
- package/js/beam.com.mjs +33 -0
- package/js/download.mjs +78 -0
- package/js/node.mjs +75 -0
- package/package.json +36 -0
- package/runtime/app-com.js +203 -0
- package/runtime/beam.mjs +2 -0
- package/runtime/beam.wasm +0 -0
- package/runtime/browser/beam-wasm.js +2 -0
- package/runtime/browser/net.js +7 -0
- package/runtime/browser/none.js +4 -0
- package/runtime/browser.js +161 -0
- package/runtime/cloudflare/durable.js +361 -0
- package/runtime/cloudflare/index.js +76 -0
- package/runtime/cloudflare/release.js +42 -0
- package/runtime/cloudflare/snapshot.js +3 -0
- package/runtime/cloudflare/worker.js +1891 -0
- package/runtime/deno/app.js +8 -0
- package/runtime/deno/beam-wasm.js +3 -0
- package/runtime/deno/none.js +3 -0
- package/runtime/deno/release-bin.js +46 -0
- package/runtime/deno/snapshot-bin.js +11 -0
- package/runtime/deno/worker.js +1891 -0
- package/runtime/deno.js +346 -0
- package/runtime/deno.json +5 -0
- package/runtime/durable-global.js +40 -0
- package/runtime/durable.js +361 -0
- package/runtime/global.js +26 -0
- package/runtime/licenses/LICENSE +202 -0
- package/runtime/licenses/NOTICE +97 -0
- package/runtime/licenses/bcrypt_elixir.txt +87 -0
- package/runtime/licenses/cosmopolitan.txt +16 -0
- package/runtime/licenses/elixir/Apache-2.0.txt +73 -0
- package/runtime/licenses/elixir/LicenseRef-elixir-trademark-policy.txt +98 -0
- package/runtime/licenses/elixir/LicenseRef-scancode-unicode.txt +58 -0
- package/runtime/licenses/emscripten.txt +102 -0
- package/runtime/licenses/exqlite.txt +21 -0
- package/runtime/licenses/llvm.txt +311 -0
- package/runtime/licenses/musl.txt +193 -0
- package/runtime/licenses/otp/BSD-2-Clause.txt +22 -0
- package/runtime/licenses/otp/BSD-3-Clause.txt +25 -0
- package/runtime/licenses/otp/LicenseRef-IETF-MIB.txt +9 -0
- package/runtime/licenses/otp/LicenseRef-RSA-PKCS3.txt +23 -0
- package/runtime/licenses/otp/LicenseRef-RSA-PKCS5v2-0.txt +17 -0
- package/runtime/licenses/otp/LicenseRef-scancode-ietf-trust.txt +296 -0
- package/runtime/licenses/otp/LicenseRef-scancode-ietf.txt +23 -0
- package/runtime/licenses/otp/LicenseRef-scancode-w3c-docs-19990405.txt +26 -0
- package/runtime/licenses/otp/LicenseRef-scancode-wxwindows-free-doc-3.txt +57 -0
- package/runtime/licenses/otp/MIT.txt +17 -0
- package/runtime/licenses/otp/SGI-B-2.0.txt +25 -0
- package/runtime/licenses/otp/W3C-19980720.txt +55 -0
- package/runtime/licenses/picosat.txt +19 -0
- package/runtime/licenses/wamr.txt +219 -0
- package/runtime/nifs.js +2 -0
- package/runtime/package.json +1 -0
- package/runtime/page/404.html +49 -0
- package/runtime/page/app-com.js +203 -0
- package/runtime/page/beam.mjs +2 -0
- package/runtime/page/browser/beam-wasm.js +2 -0
- package/runtime/page/browser/net.js +7 -0
- package/runtime/page/browser/none.js +4 -0
- package/runtime/page/browser.js +161 -0
- package/runtime/page/index.html +50 -0
- package/runtime/page/licenses/LICENSE +202 -0
- package/runtime/page/licenses/NOTICE +97 -0
- package/runtime/page/licenses/bcrypt_elixir.txt +87 -0
- package/runtime/page/licenses/cosmopolitan.txt +16 -0
- package/runtime/page/licenses/elixir/Apache-2.0.txt +73 -0
- package/runtime/page/licenses/elixir/LicenseRef-elixir-trademark-policy.txt +98 -0
- package/runtime/page/licenses/elixir/LicenseRef-scancode-unicode.txt +58 -0
- package/runtime/page/licenses/emscripten.txt +102 -0
- package/runtime/page/licenses/exqlite.txt +21 -0
- package/runtime/page/licenses/llvm.txt +311 -0
- package/runtime/page/licenses/musl.txt +193 -0
- package/runtime/page/licenses/otp/BSD-2-Clause.txt +22 -0
- package/runtime/page/licenses/otp/BSD-3-Clause.txt +25 -0
- package/runtime/page/licenses/otp/LicenseRef-IETF-MIB.txt +9 -0
- package/runtime/page/licenses/otp/LicenseRef-RSA-PKCS3.txt +23 -0
- package/runtime/page/licenses/otp/LicenseRef-RSA-PKCS5v2-0.txt +17 -0
- package/runtime/page/licenses/otp/LicenseRef-scancode-ietf-trust.txt +296 -0
- package/runtime/page/licenses/otp/LicenseRef-scancode-ietf.txt +23 -0
- package/runtime/page/licenses/otp/LicenseRef-scancode-w3c-docs-19990405.txt +26 -0
- package/runtime/page/licenses/otp/LicenseRef-scancode-wxwindows-free-doc-3.txt +57 -0
- package/runtime/page/licenses/otp/MIT.txt +17 -0
- package/runtime/page/licenses/otp/SGI-B-2.0.txt +25 -0
- package/runtime/page/licenses/otp/W3C-19980720.txt +55 -0
- package/runtime/page/licenses/picosat.txt +19 -0
- package/runtime/page/licenses/wamr.txt +219 -0
- package/runtime/page/main.js +176 -0
- package/runtime/page/runtime-id.js +3 -0
- package/runtime/page/scope.js +17 -0
- package/runtime/page/sw.js +111 -0
- package/runtime/page/vm.js +148 -0
- package/runtime/page/worker.js +1891 -0
- package/runtime/page/ws-shim.js +143 -0
- package/runtime/release/app.js +17 -0
- package/runtime/release.json +1 -0
- package/runtime/runtime-id.js +3 -0
- package/runtime/tcp-proxy.mjs +30 -0
- package/runtime/worker.js +1891 -0
package/js/beam.com.mjs
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// npx beam.com ARGS: beam.com of the version of this package, with ARGS
|
|
3
|
+
// (download.mjs). For example: npx beam.com app.erl -o app.com
|
|
4
|
+
import { spawn } from 'node:child_process';
|
|
5
|
+
import { readFileSync } from 'node:fs';
|
|
6
|
+
import { constants } from 'node:os';
|
|
7
|
+
import { command, ensure } from './download.mjs';
|
|
8
|
+
|
|
9
|
+
let release = null;
|
|
10
|
+
try {
|
|
11
|
+
release = JSON.parse(readFileSync(new URL('../runtime/release.json', import.meta.url), 'utf8'));
|
|
12
|
+
} catch {}
|
|
13
|
+
|
|
14
|
+
let file;
|
|
15
|
+
try {
|
|
16
|
+
file = await ensure(release, { log: (text) => console.error(text) });
|
|
17
|
+
} catch (e) {
|
|
18
|
+
console.error(e.message);
|
|
19
|
+
process.exit(1);
|
|
20
|
+
}
|
|
21
|
+
const [program, args] = command(file, process.argv.slice(2));
|
|
22
|
+
const child = spawn(program, args, { stdio: 'inherit' });
|
|
23
|
+
// The terminal sends SIGINT to the process group, so beam.com gets it
|
|
24
|
+
// already: a second one would go to the break handler of the BEAM.
|
|
25
|
+
process.on('SIGINT', () => {});
|
|
26
|
+
process.on('SIGTERM', () => child.kill('SIGTERM'));
|
|
27
|
+
child.on('error', (e) => {
|
|
28
|
+
console.error(`beam.com: ${file}: ${e.message}`);
|
|
29
|
+
process.exit(1);
|
|
30
|
+
});
|
|
31
|
+
child.on('exit', (code, signal) => {
|
|
32
|
+
process.exit(signal ? 128 + (constants.signals[signal] ?? 0) : (code ?? 1));
|
|
33
|
+
});
|
package/js/download.mjs
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
// The beam.com of this package: the file of the GitHub release of the same
|
|
2
|
+
// version, in the cache, with the SHA-256 that the package holds
|
|
3
|
+
// (runtime/release.json, from scripts/npm.sh). The package does not hold
|
|
4
|
+
// the file: a host that only uses the runtime never downloads it.
|
|
5
|
+
//
|
|
6
|
+
// - BEAM_COM: the path of a beam.com to use. Then nothing is downloaded.
|
|
7
|
+
// - BEAM_COM_DOWNLOAD: the URL of the directory of the files, in place of
|
|
8
|
+
// the GitHub release (a mirror, or a test).
|
|
9
|
+
// - BEAM_COM_CACHE: the cache directory, as for beam.com.
|
|
10
|
+
import { createHash } from 'node:crypto';
|
|
11
|
+
import fs from 'node:fs';
|
|
12
|
+
import os from 'node:os';
|
|
13
|
+
import path from 'node:path';
|
|
14
|
+
|
|
15
|
+
export const REPOSITORY = 'sntran/BEAM.com';
|
|
16
|
+
|
|
17
|
+
// The cache directory of beam.com (beam_com:cache_dir/0).
|
|
18
|
+
export function cacheDir(env = process.env) {
|
|
19
|
+
if (env.BEAM_COM_CACHE) return env.BEAM_COM_CACHE;
|
|
20
|
+
if (env.XDG_CACHE_HOME) return path.join(env.XDG_CACHE_HOME, 'beam.com');
|
|
21
|
+
if (env.LOCALAPPDATA) return path.join(env.LOCALAPPDATA, 'beam.com');
|
|
22
|
+
return path.join(env.HOME ?? os.homedir(), '.cache', 'beam.com');
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// The path of the file in the cache. The name stays beam.com, because
|
|
26
|
+
// beam.com selects a tool by its name (mix.com is mix). Windows runs an
|
|
27
|
+
// APE file as a PE executable only with an .exe name.
|
|
28
|
+
export function cachePath({ version, sha256 }, env = process.env, platform = process.platform) {
|
|
29
|
+
const name = platform === 'win32' ? 'beam.exe' : 'beam.com';
|
|
30
|
+
return path.join(cacheDir(env), 'npm', `${version}-${sha256.slice(0, 12)}`, name);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function downloadUrl({ version }, env = process.env) {
|
|
34
|
+
const base = env.BEAM_COM_DOWNLOAD || `https://github.com/${REPOSITORY}/releases/download/v${version}`;
|
|
35
|
+
return `${base.replace(/\/$/, '')}/beam.com`;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// The path of a beam.com that runs. The file in the cache was checked when
|
|
39
|
+
// it was downloaded: a file is in the cache only after the check.
|
|
40
|
+
export async function ensure(release, { env = process.env, platform = process.platform, fetch = globalThis.fetch, log = () => {} } = {}) {
|
|
41
|
+
if (env.BEAM_COM) return env.BEAM_COM;
|
|
42
|
+
if (!release?.version || !/^[0-9a-f]{64}$/.test(release?.sha256 ?? '')) {
|
|
43
|
+
throw new Error('this package has no release of beam.com (runtime/release.json): set BEAM_COM to the path of a beam.com');
|
|
44
|
+
}
|
|
45
|
+
const file = cachePath(release, env, platform);
|
|
46
|
+
if (fs.existsSync(file)) return file;
|
|
47
|
+
const url = downloadUrl(release, env);
|
|
48
|
+
log(`beam.com: downloading ${url}`);
|
|
49
|
+
const response = await fetch(url);
|
|
50
|
+
if (!response.ok) throw new Error(`beam.com: ${url}: status ${response.status}`);
|
|
51
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
52
|
+
const part = `${file}.${process.pid}.part`;
|
|
53
|
+
const out = fs.createWriteStream(part, { mode: 0o755 });
|
|
54
|
+
const hash = createHash('sha256');
|
|
55
|
+
try {
|
|
56
|
+
for await (const chunk of response.body) {
|
|
57
|
+
hash.update(chunk);
|
|
58
|
+
if (!out.write(chunk)) await new Promise((r) => out.once('drain', r));
|
|
59
|
+
}
|
|
60
|
+
await new Promise((resolve, reject) => out.end((e) => (e ? reject(e) : resolve())));
|
|
61
|
+
const got = hash.digest('hex');
|
|
62
|
+
if (got !== release.sha256) {
|
|
63
|
+
throw new Error(`beam.com: ${url}: the SHA-256 is ${got}, and this package expects ${release.sha256}`);
|
|
64
|
+
}
|
|
65
|
+
fs.chmodSync(part, 0o755);
|
|
66
|
+
fs.renameSync(part, file);
|
|
67
|
+
} finally {
|
|
68
|
+
out.destroy();
|
|
69
|
+
fs.rmSync(part, { force: true });
|
|
70
|
+
}
|
|
71
|
+
return file;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// The command that starts the file: Windows runs it as a PE executable,
|
|
75
|
+
// and a Unix shell runs the shell script at the start of an APE file.
|
|
76
|
+
export function command(file, args, platform = process.platform) {
|
|
77
|
+
return platform === 'win32' ? [file, args] : ['/bin/sh', [file, ...args]];
|
|
78
|
+
}
|
package/js/node.mjs
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
// The BEAM in Node.js (25 or later, for JSPI): a native app.com (beam.com
|
|
2
|
+
// INPUT -o app.com) with the WebAssembly runtime of this package.
|
|
3
|
+
//
|
|
4
|
+
// import { boot } from 'beam.com';
|
|
5
|
+
// const vm = await boot('app.com', { env: { PORT: '4000' } });
|
|
6
|
+
// const response = await vm.fetch(new Request('http://localhost/'));
|
|
7
|
+
//
|
|
8
|
+
// worker.js of the runtime is the code of the Workers. Its TCP sockets use
|
|
9
|
+
// node:net. It imports beam.wasm as a WebAssembly.Module, as in Workers,
|
|
10
|
+
// and Node.js imports a .wasm file as an instance: the hook gives the
|
|
11
|
+
// module to the files of runtime/ only.
|
|
12
|
+
import { registerHooks } from 'node:module';
|
|
13
|
+
import fs from 'node:fs/promises';
|
|
14
|
+
import { fileURLToPath } from 'node:url';
|
|
15
|
+
// app-com.js and runtime-id.js import nothing: they load before the hooks.
|
|
16
|
+
import { appFiles, appRelease } from '../runtime/app-com.js';
|
|
17
|
+
import runtimeId from '../runtime/runtime-id.js';
|
|
18
|
+
|
|
19
|
+
const runtimeUrl = new URL('../runtime/', import.meta.url);
|
|
20
|
+
const wasmUrl = new URL('beam.wasm', runtimeUrl);
|
|
21
|
+
const WASM = 'beam-com:beam.wasm';
|
|
22
|
+
|
|
23
|
+
registerHooks({
|
|
24
|
+
resolve(spec, ctx, next) {
|
|
25
|
+
if (spec === './beam.wasm' && ctx.parentURL?.startsWith(runtimeUrl.href)) {
|
|
26
|
+
return { url: WASM, shortCircuit: true };
|
|
27
|
+
}
|
|
28
|
+
return next(spec, ctx);
|
|
29
|
+
},
|
|
30
|
+
load(url, ctx, next) {
|
|
31
|
+
if (url !== WASM) return next(url, ctx);
|
|
32
|
+
return {
|
|
33
|
+
format: 'module',
|
|
34
|
+
shortCircuit: true,
|
|
35
|
+
source: 'import fs from "node:fs";\n'
|
|
36
|
+
+ `export default await WebAssembly.compile(fs.readFileSync(${JSON.stringify(fileURLToPath(wasmUrl))}));\n`,
|
|
37
|
+
};
|
|
38
|
+
},
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
export { appRelease, runtimeId };
|
|
42
|
+
|
|
43
|
+
// The release of an app.com for this runtime (release.bin as an
|
|
44
|
+
// ArrayBuffer). The file must come from the beam.com of the version of
|
|
45
|
+
// this package: app-com.js refuses a file for another runtime.
|
|
46
|
+
export function release(app) {
|
|
47
|
+
return readApp(app, appRelease);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// The release of app with get (appRelease, or appFiles: no copy).
|
|
51
|
+
async function readApp(app, get) {
|
|
52
|
+
const file = await fs.open(app);
|
|
53
|
+
try {
|
|
54
|
+
const { size } = await file.stat();
|
|
55
|
+
const read = async (at, n) => {
|
|
56
|
+
const b = new Uint8Array(n);
|
|
57
|
+
const { bytesRead } = await file.read(b, 0, n, at);
|
|
58
|
+
return b.subarray(0, bytesRead);
|
|
59
|
+
};
|
|
60
|
+
return await get(read, size, { runtime: runtimeId });
|
|
61
|
+
} finally {
|
|
62
|
+
await file.close();
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// The VM of an app.com, when it is ready. env: the variables of the VM.
|
|
67
|
+
// There is no cache for snapshots in Node.js, so BEAM_SNAPSHOT is "off".
|
|
68
|
+
// The VM keeps its state between requests (plain: false), as in Deno.
|
|
69
|
+
export async function boot(app, { env = {} } = {}) {
|
|
70
|
+
const bin = await readApp(app, appFiles);
|
|
71
|
+
const { Vm } = await import(new URL('worker.js', runtimeUrl).href);
|
|
72
|
+
const vm = new Vm({ BEAM_SNAPSHOT: 'off', ...env }, { release: bin, plain: false });
|
|
73
|
+
await vm.ready;
|
|
74
|
+
return vm;
|
|
75
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "beam.com",
|
|
3
|
+
"version": "0.1.0-rc.1",
|
|
4
|
+
"description": "Erlang/OTP and Elixir in one executable file: npx beam.com, and the WebAssembly runtime for a native app.com",
|
|
5
|
+
"license": "Apache-2.0",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/sntran/BEAM.com.git"
|
|
9
|
+
},
|
|
10
|
+
"homepage": "https://github.com/sntran/BEAM.com#readme",
|
|
11
|
+
"engines": {
|
|
12
|
+
"node": ">=25"
|
|
13
|
+
},
|
|
14
|
+
"bin": {
|
|
15
|
+
"beam.com": "js/beam.com.mjs"
|
|
16
|
+
},
|
|
17
|
+
"exports": {
|
|
18
|
+
".": {
|
|
19
|
+
"workerd": "./runtime/cloudflare/index.js",
|
|
20
|
+
"deno": "./runtime/deno.js",
|
|
21
|
+
"node": "./js/node.mjs",
|
|
22
|
+
"default": "./js/node.mjs"
|
|
23
|
+
},
|
|
24
|
+
"./app-com": "./runtime/app-com.js",
|
|
25
|
+
"./runtime-id": "./runtime/runtime-id.js",
|
|
26
|
+
"./worker": "./runtime/worker.js",
|
|
27
|
+
"./beam.wasm": "./runtime/beam.wasm",
|
|
28
|
+
"./package.json": "./package.json"
|
|
29
|
+
},
|
|
30
|
+
"files": [
|
|
31
|
+
"js/",
|
|
32
|
+
"runtime/",
|
|
33
|
+
"LICENSE",
|
|
34
|
+
"NOTICE"
|
|
35
|
+
]
|
|
36
|
+
}
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
// The release of a native app.com for the WebAssembly runtime: the same
|
|
2
|
+
// file that runs natively runs in the VM of worker.js (Workers, Deno, a
|
|
3
|
+
// web page). "beam.com INPUT -o app.com" writes the edge part of the file
|
|
4
|
+
// into its zip, under .wasm/ (beam_com_wasm:overlay/4): .release.json
|
|
5
|
+
// (the boot of the VM, and the runtime of the build), the application
|
|
6
|
+
// wasm_host, the boot script with wasm_host, and the modules in the place
|
|
7
|
+
// of NIFs.
|
|
8
|
+
//
|
|
9
|
+
// appRelease(read, size, { runtime }) gives the release as release.bin
|
|
10
|
+
// gives it (an ArrayBuffer, see unpack in worker.js): the files of lib/
|
|
11
|
+
// and releases/ of the zip, with the files of .wasm/ in their place, and
|
|
12
|
+
// .release.json first.
|
|
13
|
+
// - read(at, n): n bytes (a Uint8Array) at the offset at of app.com: a
|
|
14
|
+
// file, a range of a URL, or bytes in memory (bytesReader).
|
|
15
|
+
// - size: the size of app.com in bytes.
|
|
16
|
+
// - runtime: the identity of this runtime (the text of runtime-id.js of
|
|
17
|
+
// DIR, beam_com_wasm:runtime_id/1). With it, a file for another runtime
|
|
18
|
+
// is an error.
|
|
19
|
+
// It reads three parts: the end of the file, the central directory, and
|
|
20
|
+
// the span of the release (the edge part is at the end of it).
|
|
21
|
+
//
|
|
22
|
+
// appFiles(read, size, { runtime }) gives the same files with no copy:
|
|
23
|
+
// {meta, files, byteLength, buffers, statics}, which worker.js takes in
|
|
24
|
+
// the place of release.bin.
|
|
25
|
+
// - files: [path, bytes] for each file of the release. A stored entry is a
|
|
26
|
+
// view of the bytes that read gives (with bytesReader, of app.com).
|
|
27
|
+
// - buffers: the ArrayBuffers of these bytes. A file of the VM in another
|
|
28
|
+
// buffer is a file that the boot wrote (the snapshot keeps it).
|
|
29
|
+
// - statics: the static files of a Phoenix app (lib/NAME-VSN/priv/static,
|
|
30
|
+
// NAME the name of the release), a Map of path to {data, size, crc, deflated}, which
|
|
31
|
+
// the host serves before the VM (worker.js). They are not in files,
|
|
32
|
+
// except cache_manifest.json, which Phoenix reads at its start.
|
|
33
|
+
//
|
|
34
|
+
// The zip of beam.com stores each .beam file as a gzip file (the loader of
|
|
35
|
+
// ERTS reads it so, beam_com_zip), so it is a view too. A deflated .beam
|
|
36
|
+
// entry (an older app.com) becomes a gzip file with the same data, with
|
|
37
|
+
// no inflate. Each other entry is checked with its CRC-32.
|
|
38
|
+
|
|
39
|
+
const text = new TextDecoder();
|
|
40
|
+
const u16 = (b, i) => b[i] | (b[i + 1] << 8);
|
|
41
|
+
const u32 = (b, i) => (b[i] | (b[i + 1] << 8) | (b[i + 2] << 16) | (b[i + 3] << 24)) >>> 0;
|
|
42
|
+
const fail = (message) => { throw new Error(`app.com: ${message}`); };
|
|
43
|
+
|
|
44
|
+
const LOCAL = 0x04034b50, CENTRAL = 0x02014b50, END = 0x06054b50;
|
|
45
|
+
const STORED = 0, DEFLATED = 8;
|
|
46
|
+
// The end record (22 bytes) and a comment of at most 65535 bytes.
|
|
47
|
+
const TAIL = 22 + 0xffff;
|
|
48
|
+
|
|
49
|
+
const CRC = new Uint32Array(256).map((_, n) => {
|
|
50
|
+
let c = n;
|
|
51
|
+
for (let k = 0; k < 8; k++) c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1;
|
|
52
|
+
return c >>> 0;
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
export function crc32(b) {
|
|
56
|
+
let c = 0xffffffff;
|
|
57
|
+
for (let i = 0; i < b.length; i++) c = CRC[(c ^ b[i]) & 0xff] ^ (c >>> 8);
|
|
58
|
+
return (c ^ 0xffffffff) >>> 0;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// A reader of bytes in memory (an ArrayBuffer or a typed array).
|
|
62
|
+
export function bytesReader(bytes) {
|
|
63
|
+
const b = bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes.buffer ?? bytes, bytes.byteOffset ?? 0, bytes.byteLength);
|
|
64
|
+
return { size: b.length, read: async (at, n) => b.subarray(at, at + n) };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// The central directory: {entries, cdAt}, where each entry is {name,
|
|
68
|
+
// method, crc, csize, size, offset}.
|
|
69
|
+
export async function zipEntries(read, size) {
|
|
70
|
+
const n = Math.min(size, TAIL);
|
|
71
|
+
const tail = await read(size - n, n);
|
|
72
|
+
if (tail.length !== n) fail('a short read at the end of the file');
|
|
73
|
+
// The last end record whose comment ends the file.
|
|
74
|
+
let e = -1;
|
|
75
|
+
for (let i = n - 22; i >= 0; i--) {
|
|
76
|
+
if (u32(tail, i) === END && i + 22 + u16(tail, i + 20) === n) { e = i; break; }
|
|
77
|
+
}
|
|
78
|
+
if (e < 0) fail('not a zip file (no end of central directory)');
|
|
79
|
+
const count = u16(tail, e + 10), cdSize = u32(tail, e + 12), cdAt = u32(tail, e + 16);
|
|
80
|
+
if (count === 0xffff || cdAt === 0xffffffff || cdSize === 0xffffffff) fail('a zip64 file is not supported');
|
|
81
|
+
if (cdAt + cdSize > size) fail('the central directory is outside the file');
|
|
82
|
+
const cd = await read(cdAt, cdSize);
|
|
83
|
+
const entries = [];
|
|
84
|
+
for (let i = 0, k = 0; k < count; k++) {
|
|
85
|
+
if (i + 46 > cd.length || u32(cd, i) !== CENTRAL) fail('a bad central directory');
|
|
86
|
+
const nlen = u16(cd, i + 28), xlen = u16(cd, i + 30), clen = u16(cd, i + 32);
|
|
87
|
+
entries.push({
|
|
88
|
+
name: text.decode(cd.subarray(i + 46, i + 46 + nlen)), method: u16(cd, i + 10),
|
|
89
|
+
crc: u32(cd, i + 16), csize: u32(cd, i + 20), size: u32(cd, i + 24), offset: u32(cd, i + 42),
|
|
90
|
+
});
|
|
91
|
+
i += 46 + nlen + xlen + clen;
|
|
92
|
+
}
|
|
93
|
+
return { entries, cdAt };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async function inflate(raw) {
|
|
97
|
+
const s = new Blob([raw]).stream().pipeThrough(new DecompressionStream('deflate-raw'));
|
|
98
|
+
return new Uint8Array(await new Response(s).arrayBuffer());
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// The same deflate data in a gzip file: a header of 10 bytes, the data,
|
|
102
|
+
// the CRC-32 and the size.
|
|
103
|
+
function gzipOf(raw, e) {
|
|
104
|
+
const out = new Uint8Array(10 + raw.length + 8);
|
|
105
|
+
out.set([0x1f, 0x8b, 8, 0, 0, 0, 0, 0, 0, 255]);
|
|
106
|
+
out.set(raw, 10);
|
|
107
|
+
const v = new DataView(out.buffer);
|
|
108
|
+
v.setUint32(10 + raw.length, e.crc, true);
|
|
109
|
+
v.setUint32(14 + raw.length, e.size, true);
|
|
110
|
+
return out;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// The data of the entry e, in span (the bytes of the file from offset base).
|
|
114
|
+
async function entryData(span, base, e) {
|
|
115
|
+
const h = e.offset - base;
|
|
116
|
+
if (h < 0 || h + 30 > span.length || u32(span, h) !== LOCAL) fail(`${e.name}: no local header`);
|
|
117
|
+
const start = h + 30 + u16(span, h + 26) + u16(span, h + 28);
|
|
118
|
+
if (start + e.csize > span.length) fail(`${e.name}: the data is outside the file`);
|
|
119
|
+
const raw = span.subarray(start, start + e.csize);
|
|
120
|
+
if (e.method === DEFLATED && e.name.endsWith('.beam')) return gzipOf(raw, e);
|
|
121
|
+
let d;
|
|
122
|
+
if (e.method === STORED) d = raw;
|
|
123
|
+
else if (e.method === DEFLATED) d = await inflate(raw);
|
|
124
|
+
else fail(`${e.name}: the compression method ${e.method} is not supported`);
|
|
125
|
+
if (d.length !== e.size || crc32(d) !== e.crc) fail(`${e.name}: the CRC-32 does not match`);
|
|
126
|
+
return d;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const EDGE = '.wasm/';
|
|
130
|
+
const inRelease = (name) => name.startsWith('lib/') || name.startsWith('releases/') || name.startsWith(EDGE);
|
|
131
|
+
|
|
132
|
+
export async function appRelease(read, size, options = {}) {
|
|
133
|
+
const { meta, files } = await appFiles(read, size, { ...options, statics: false });
|
|
134
|
+
return pack(meta, files);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export async function appFiles(read, size, { runtime, statics: split = true } = {}) {
|
|
138
|
+
const { entries, cdAt } = await zipEntries(read, size);
|
|
139
|
+
const want = entries.filter((e) => inRelease(e.name) && !e.name.endsWith('/'));
|
|
140
|
+
const json = want.find((e) => e.name === `${EDGE}.release.json`);
|
|
141
|
+
if (!json) fail('no edge part (.wasm/.release.json): build it with a beam.com that has the WebAssembly runtime, and without --no-edge');
|
|
142
|
+
// One read: the entries of the release, up to the central directory.
|
|
143
|
+
const lo = Math.min(...want.map((e) => e.offset));
|
|
144
|
+
const span = await read(lo, cdAt - lo);
|
|
145
|
+
if (span.length !== cdAt - lo) fail('a short read of the release');
|
|
146
|
+
const meta = await entryData(span, lo, json);
|
|
147
|
+
const release = JSON.parse(text.decode(meta));
|
|
148
|
+
if (runtime && release.runtime !== runtime) {
|
|
149
|
+
fail(`it was built for the runtime ${String(release.runtime).slice(0, 12)}, and this runtime is ${runtime.slice(0, 12)}: `
|
|
150
|
+
+ 'use the runtime of the beam.com that built it');
|
|
151
|
+
}
|
|
152
|
+
// The static files of a Phoenix app (PHX_SERVER in the environment of
|
|
153
|
+
// the release): lib/NAME-VSN/priv/static/, for the application with the
|
|
154
|
+
// name of the release. Plug.Static serves them at the root of the site.
|
|
155
|
+
const app = split && release.env?.PHX_SERVER === 'true' && typeof release.name === 'string'
|
|
156
|
+
&& `lib/${release.name}-`;
|
|
157
|
+
const prefix = app && want.map((e) => /^lib\/[^/]+\/priv\/static\//.exec(e.name)?.[0])
|
|
158
|
+
.find((p) => p?.startsWith(app) && /^[0-9]/.test(p.slice(app.length)));
|
|
159
|
+
const statics = new Map();
|
|
160
|
+
const files = new Map();
|
|
161
|
+
for (const e of want) {
|
|
162
|
+
if (e.name.startsWith(EDGE)) continue;
|
|
163
|
+
if (prefix && e.name.startsWith(prefix) && e.name !== `${prefix}cache_manifest.json`) {
|
|
164
|
+
statics.set(e.name.slice(prefix.length - 1), staticEntry(span, lo, e));
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
files.set(e.name, await entryData(span, lo, e));
|
|
168
|
+
}
|
|
169
|
+
for (const e of want) if (e.name.startsWith(EDGE) && e !== json) files.set(e.name.slice(EDGE.length), await entryData(span, lo, e));
|
|
170
|
+
const list = [...files];
|
|
171
|
+
return {
|
|
172
|
+
meta, files: list, statics,
|
|
173
|
+
byteLength: list.reduce((n, [, d]) => n + d.length, meta.length),
|
|
174
|
+
buffers: new Set([meta.buffer, ...list.map(([, d]) => d.buffer)]),
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// A static file: its bytes in the zip (stored or deflated), not checked
|
|
179
|
+
// here (the host checks the CRC-32 when it inflates them).
|
|
180
|
+
function staticEntry(span, base, e) {
|
|
181
|
+
const h = e.offset - base;
|
|
182
|
+
if (h < 0 || h + 30 > span.length || u32(span, h) !== LOCAL) fail(`${e.name}: no local header`);
|
|
183
|
+
const start = h + 30 + u16(span, h + 26) + u16(span, h + 28);
|
|
184
|
+
if (start + e.csize > span.length) fail(`${e.name}: the data is outside the file`);
|
|
185
|
+
if (e.method !== STORED && e.method !== DEFLATED) fail(`${e.name}: the compression method ${e.method} is not supported`);
|
|
186
|
+
return { data: span.subarray(start, start + e.csize), size: e.size, crc: e.crc, deflated: e.method === DEFLATED };
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// release.bin: "BEAMFS1\n", then (length, path, length, data) for each
|
|
190
|
+
// file, with .release.json first.
|
|
191
|
+
function pack(meta, files) {
|
|
192
|
+
const enc = new TextEncoder();
|
|
193
|
+
const parts = [['.release.json', meta], ...files].map(([p, d]) => [enc.encode(p), d]);
|
|
194
|
+
const out = new Uint8Array(8 + parts.reduce((s, [p, d]) => s + 8 + p.length + d.length, 0));
|
|
195
|
+
const v = new DataView(out.buffer);
|
|
196
|
+
out.set(enc.encode('BEAMFS1\n'));
|
|
197
|
+
let i = 8;
|
|
198
|
+
for (const [p, d] of parts) {
|
|
199
|
+
v.setUint32(i, p.length); out.set(p, i + 4); i += 4 + p.length;
|
|
200
|
+
v.setUint32(i, d.length); out.set(d, i + 4); i += 4 + d.length;
|
|
201
|
+
}
|
|
202
|
+
return out.buffer;
|
|
203
|
+
}
|