mirror of
https://github.com/HeyPuter/gex.git
synced 2026-11-13 20:12:43 -10:00
Browser memory64 (docs/mem64.md step 6): the wasm64 shim runs x86-64 programs under node
- shims/browser builds for wasm64-unknown-unknown (GEX_MEM64=1 build.sh): gex_browser.x86.m64.shared.wasm, a 16 GiB memory64; its JS exports and imports have the same types as the wasm32 shim's. - gexc seed --mem64; gex-emit's dense dispatch addresses memory in its index type (wasm32 output byte-identical). - The global kernel for such processes (GEX_MEM64=1 shims/kernel/build.sh, gex_kernel.x86.m64.wasm): gex-kmem's process64 feature addresses the process memory with i64; add-memory.mjs can declare a memory64. - Glue: GEX_MEM64 (run-node, carried to Workers) creates the memory64, grows it in BigInt pages and sets the wasm64 shim's i64 __stack_pointer. GEX_DUMP_BAD_WASM=DIR keeps a rejected module under node. ack, bits and branchy (gx64) print the same as on the wasm32 shim. Guest addresses passed to the kernel are still u32 (buffers below 4 GiB). Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]> Claude-Session: https://claude.ai/code/session_01PeRutwFgZ66XpxjasKrPn1
This commit is contained in:
1 parent
3bd52232a3
commit
36e98cd5c5
14 files changed
+112
-16
No files matched your search
@@ -255,7 +255,7 @@ pub fn emit_arm(f: &mut crate::wasm::FuncBody, k: usize, ctx: u32, index: u32, s
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn emit_arm_at(f: &mut crate::wasm::FuncBody, k: usize, ctx: u32, index: u32, scratch: u32, block_ty: u32, classic: u32, blocks: u32) {
|
||||
debug_assert!(k >= FIRST_TABLE);
|
||||
f.i32_const(0);
|
||||
f.addr_const(0);
|
||||
f.load(0x28, 2, (MAPS + 4 * (k - FIRST_TABLE) as u64) as u32); // i32.load
|
||||
f.local_tee(scratch);
|
||||
f.if_void();
|
||||
@@ -265,6 +265,7 @@ pub fn emit_arm_at(f: &mut crate::wasm::FuncBody, k: usize, ctx: u32, index: u32
|
||||
f.i32_const(2);
|
||||
f.byte(0x74); // i32.shl
|
||||
f.byte(0x6a); // i32.add
|
||||
f.addr_i32(); // the map is in the runtime's low memory
|
||||
f.load(0x28, 2, 0); // i32.load: the block's index in `blocks`
|
||||
f.return_call_indirect(block_ty, blocks);
|
||||
f.end();
|
||||
@@ -287,6 +288,7 @@ pub fn dispatch_body(block_ty: u32, first: u32, blocks: u32) -> Vec<u8> {
|
||||
let entry = f.add_local(ValType::I32);
|
||||
let index = f.add_local(ValType::I32);
|
||||
f.local_get(0);
|
||||
f.addr_i32();
|
||||
f.load(0x29, 3, ctx::LAST_TARGET as u32);
|
||||
f.local_set(target);
|
||||
f.local_get(target);
|
||||
@@ -297,6 +299,7 @@ pub fn dispatch_body(block_ty: u32, first: u32, blocks: u32) -> Vec<u8> {
|
||||
f.byte(0xa7); // i32.wrap_i64
|
||||
f.i32_const(4);
|
||||
f.byte(0x6c); // i32.mul
|
||||
f.addr_i32();
|
||||
f.load(0x28, 2, DISPATCH_DIR as u32);
|
||||
f.i32_const(0);
|
||||
f.local_get(page);
|
||||
|
||||
@@ -109,7 +109,11 @@ impl<'a> GuestMem<'a> {
|
||||
fn limit(&self) -> usize {
|
||||
#[cfg(target_family = "wasm")]
|
||||
{
|
||||
let live = (core::arch::wasm32::memory_size(0) as u64) << 16;
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
let pages = core::arch::wasm32::memory_size(0);
|
||||
#[cfg(target_arch = "wasm64")]
|
||||
let pages = core::arch::wasm64::memory_size(0);
|
||||
let live = (pages as u64) << 16;
|
||||
(live as usize).min(self.view().len())
|
||||
}
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
|
||||
@@ -5,3 +5,8 @@ edition.workspace = true
|
||||
|
||||
[lib]
|
||||
crate-type = ["rlib"]
|
||||
|
||||
[features]
|
||||
# The process memory is a memory64 (an x86-64 guest with a 64-bit memory,
|
||||
# docs/mem64.md step 6): its addresses are i64 in `memory.copy`/`memory.size`.
|
||||
process64 = []
|
||||
@@ -25,7 +25,10 @@ pub const PROCESS_MEMORY: u32 = 1;
|
||||
/// rather than trapping the Worker mid-syscall with the kernel lock held.
|
||||
#[inline]
|
||||
pub fn process_pages() -> u32 {
|
||||
#[cfg(not(feature = "process64"))]
|
||||
let n: u32;
|
||||
#[cfg(feature = "process64")]
|
||||
let n: u64;
|
||||
unsafe {
|
||||
core::arch::asm!(
|
||||
"memory.size 1",
|
||||
@@ -33,7 +36,7 @@ pub fn process_pages() -> u32 {
|
||||
n = out(local) n,
|
||||
);
|
||||
}
|
||||
n
|
||||
n as u32
|
||||
}
|
||||
|
||||
/// Whether `at..at+len` is inside the calling process's memory. Every `push`
|
||||
@@ -57,6 +60,10 @@ pub fn in_process(at: u32, len: u32) -> bool {
|
||||
/// syscall that has already taken the kernel lock.
|
||||
#[inline]
|
||||
pub unsafe fn pull(dst: *mut u8, src: u32, len: u32) {
|
||||
// A memory64 process memory takes an i64 address; the kernel's own (and
|
||||
// the length, the smaller of the two index types) stay i32.
|
||||
#[cfg(feature = "process64")]
|
||||
let src = src as u64;
|
||||
unsafe {
|
||||
core::arch::asm!(
|
||||
"local.get {dst}",
|
||||
@@ -76,6 +83,8 @@ pub unsafe fn pull(dst: *mut u8, src: u32, len: u32) {
|
||||
/// As [`pull`], with the roles reversed.
|
||||
#[inline]
|
||||
pub unsafe fn push(dst: u32, src: *const u8, len: u32) {
|
||||
#[cfg(feature = "process64")]
|
||||
let dst = dst as u64;
|
||||
unsafe {
|
||||
core::arch::asm!(
|
||||
"local.get {dst}",
|
||||
|
||||
@@ -107,7 +107,7 @@ usage:
|
||||
through DT_NEEDED, each at the base a
|
||||
pinned run places it, writing one module
|
||||
per object and the layout beside them)
|
||||
gexc seed -o <out.wasm> write a module that translates nothing, for
|
||||
gexc seed -o <out.wasm> [--mem64] write a module that translates nothing, for
|
||||
a host that will translate at runtime and
|
||||
does not know the program in advance
|
||||
gexc closure <elf> --sysroot DIR
|
||||
@@ -717,6 +717,8 @@ fn cmd_seed(args: &[String]) -> Result<()> {
|
||||
out = Some(args.get(i).context("-o needs a path")?.clone());
|
||||
}
|
||||
"--no-shared-memory" => shared_memory = false,
|
||||
// The seed for the wasm64 (memory64) browser shim.
|
||||
"--mem64" => gex_abi::set_mem_flavor(gex_abi::MemFlavor::M64),
|
||||
other => bail!("unexpected argument {other}"),
|
||||
}
|
||||
i += 1;
|
||||
|
||||
@@ -219,6 +219,28 @@ $GEXC seed -o web/public/gex/seed$INFIX.wasm --no-shared-memory >/dev/null
|
||||
$GEXC seed -o web/public/gex/seed$INFIX.shared.wasm >/dev/null
|
||||
echo " seed: rebuilt from this emitter"
|
||||
|
||||
# `GEX_MEM64=1`: also the wasm64 shim for a 64-bit guest memory (docs/mem64.md
|
||||
# step 6; the Unity games on .NET, docs/unity-dotnet.md P5), x86-64 guests
|
||||
# only. Same source, target wasm64-unknown-unknown (nightly, build-std),
|
||||
# shared memory only; its memory is a memory64 of LIMIT64 (16 GiB). The
|
||||
# layout below 4 GiB is the wasm32 one. Artefacts get `.m64` after the arch.
|
||||
if [ -n "${GEX_MEM64:-}" ] && [ -n "$ARCH_FEAT" ]; then
|
||||
M64OUT=target/w64$TSUF/wasm64-unknown-unknown/release/gex_browser.wasm
|
||||
LINK64=$(echo "$LINK" | sed "s/--max-memory=$GUEST_BYTES/--max-memory=$((16 << 30))/")
|
||||
if RUSTFLAGS="$LINK64 \
|
||||
-C link-arg=--shared-memory \
|
||||
-C target-feature=+atomics,+bulk-memory,+mutable-globals \
|
||||
-Zcrate-attr=feature(simd_wasm64)" \
|
||||
cargo +nightly build -p gex-browser --target wasm64-unknown-unknown \
|
||||
--release -Z build-std=std,panic_abort --features "shared-memory$EXTRA" \
|
||||
--target-dir target/w64$TSUF 2>&1 | grep -E "^error" -A5; then
|
||||
echo "shims/browser: the wasm64 build FAILED"; exit 1
|
||||
fi
|
||||
cp "$M64OUT" "$PUB/gex_browser$INFIX.m64.shared.wasm"
|
||||
$GEXC seed --mem64 -o web/public/gex/seed$INFIX.m64.shared.wasm >/dev/null
|
||||
echo "shims/browser: $(stat -c%s "$M64OUT") bytes (wasm64, memory64), seed$INFIX.m64.shared.wasm"
|
||||
fi
|
||||
|
||||
# And the site, which is a copy of public/ and will otherwise keep serving
|
||||
# the shim that was here a moment ago.
|
||||
web/refresh-dist.sh
|
||||
@@ -7,6 +7,16 @@
|
||||
// `gex_abi::GUEST_MEM_INITIAL_BYTES`.
|
||||
const GUEST_BYTES = 0xffff0000;
|
||||
|
||||
// A 64-bit guest memory (docs/mem64.md step 6): the wasm64 shim and seed, a
|
||||
// memory64 of `gex_abi::LIMIT64` (16 GiB). The shim's own addresses (its
|
||||
// stack, data and the low runtime words) stay below 4 GiB either way; only
|
||||
// the memory's type and the i64 globals of a wasm64 module differ.
|
||||
// Read when used, not at import: a Worker learns it from its first message.
|
||||
const m64 = () => !!globalThis.GEX_MEM64;
|
||||
const LIMIT64_BYTES = 16 * 2 ** 30;
|
||||
/** A value for an address-typed global: i64 (BigInt) in a wasm64 module. */
|
||||
const addrVal = (v) => (m64() ? BigInt(v >>> 0) : v | 0);
|
||||
|
||||
/**
|
||||
* Decode UTF-8 out of guest memory.
|
||||
*
|
||||
@@ -593,7 +603,10 @@ export class Gex {
|
||||
// new one -- which is what a *thread* is: a second Worker on the same
|
||||
// memory, seeing the same guest and kernel. A new memory is a new process.
|
||||
this.#memory =
|
||||
memory ?? new WebAssembly.Memory({ initial: INITIAL_PAGES, maximum: PAGES, shared });
|
||||
memory ??
|
||||
(m64()
|
||||
? new WebAssembly.Memory({ address: 'i64', initial: BigInt(INITIAL_PAGES), maximum: BigInt(LIMIT64_BYTES / 65536), shared })
|
||||
: new WebAssembly.Memory({ initial: INITIAL_PAGES, maximum: PAGES, shared }));
|
||||
this.#kernelSpec = kernel;
|
||||
}
|
||||
|
||||
@@ -616,7 +629,7 @@ export class Gex {
|
||||
// low region -- Workers starting together get different ones.
|
||||
if (typeof this.#shim.exports.gex_worker_init?.call === 'function' && this.#memory.buffer instanceof SharedArrayBuffer) {
|
||||
const k = Atomics.add(new Int32Array(this.#memory.buffer), BOOT_STACK_COUNTER >> 2, 1) & 31;
|
||||
this.#shim.exports.__stack_pointer.value = (SHIM_STACK_TOP - k * 0x40000) | 0;
|
||||
this.#shim.exports.__stack_pointer.value = addrVal(SHIM_STACK_TOP - k * 0x40000);
|
||||
}
|
||||
const at = this.#shim.exports.gex_worker_init?.();
|
||||
this.#stackTop = at ? at | 0 : SHIM_STACK_TOP | 0;
|
||||
@@ -634,7 +647,7 @@ export class Gex {
|
||||
if (end <= have) return;
|
||||
// A 64-bit memory counts its pages in BigInt, here as everywhere else.
|
||||
const pages = Math.ceil((end - have) / 65536);
|
||||
this.#memory.grow(pages);
|
||||
this.#memory.grow(m64() ? BigInt(pages) : pages);
|
||||
this.#u8 = null;
|
||||
}
|
||||
|
||||
@@ -945,7 +958,7 @@ export class Gex {
|
||||
// First, before any call into the shim: `gex_worker_init` gives this Worker
|
||||
// its thread-local block, and until it runs `__tls_base` is zero, so a shim
|
||||
// function touching a thread-local writes through a null base.
|
||||
this.#shim.exports.__stack_pointer.value = this.#workerStack();
|
||||
this.#shim.exports.__stack_pointer.value = addrVal(this.#workerStack());
|
||||
// An older shim loads but fails on the first call to something it lacks,
|
||||
// far from the mistake. Named here so the skew is a sentence, not a mystery.
|
||||
const required = [
|
||||
@@ -978,7 +991,7 @@ export class Gex {
|
||||
if (Gex.watchDir >= 0) this.#shim.exports.gex_watch_dir?.(BigInt(Gex.watchDir));
|
||||
// wasm-ld puts the shim's stack at address zero (the guest's image); move
|
||||
// it into the shim's own region, above everything the guest maps.
|
||||
this.#shim.exports.__stack_pointer.value = this.#workerStack();
|
||||
this.#shim.exports.__stack_pointer.value = addrVal(this.#workerStack());
|
||||
// Carried into every fork child's Worker (see the fork record's `opts`), so
|
||||
// a debugging switch set for the run holds for the processes it spawns --
|
||||
// the block trace of a child that crashes is otherwise silent.
|
||||
@@ -1397,7 +1410,7 @@ export class Gex {
|
||||
this.#u8 = null;
|
||||
// The stack pointer is a per-instance global, so every Worker has its own
|
||||
// and must be told where it goes.
|
||||
this.#shim.exports.__stack_pointer.value = this.#workerStack();
|
||||
this.#shim.exports.__stack_pointer.value = addrVal(this.#workerStack());
|
||||
// The devices this process has -- process-wide flags in shared memory, so
|
||||
// only a *process* sets them. A thread answering for itself would turn a
|
||||
// GPU or network off underneath the threads using it. Before the kernel is
|
||||
@@ -2925,7 +2938,7 @@ export class Gex {
|
||||
// that restore the shim's shadow-stack global, leaking a few dozen
|
||||
// bytes each. Nothing is live below here, so reset it exactly -- left
|
||||
// alone it slid into the guest's stack and overwrote saved registers.
|
||||
this.#shim.exports.__stack_pointer.value = this.#workerStack();
|
||||
this.#shim.exports.__stack_pointer.value = addrVal(this.#workerStack());
|
||||
const e = this.#shim.exports;
|
||||
// A slot the shim wants written before the guest goes back to it: its
|
||||
// module is instantiated with passive segments, so the code is present
|
||||
@@ -3934,6 +3947,9 @@ export class Gex {
|
||||
// the page (tests/nt's harness keeps them), synchronously, since the
|
||||
// Worker may be about to die. Best effort.
|
||||
if (bytes instanceof Uint8Array && err instanceof WebAssembly.CompileError) {
|
||||
// Under Node, `GEX_DUMP_BAD_WASM=DIR` keeps it, as gexc does natively.
|
||||
const dir = globalThis.process?.env?.GEX_DUMP_BAD_WASM;
|
||||
if (dir) try { globalThis.process.getBuiltinModule('fs').writeFileSync(`${dir}/bad-${who}-${Date.now()}.wasm`, bytes); } catch {}
|
||||
try {
|
||||
const x = new XMLHttpRequest();
|
||||
x.open('POST', `/__gex_dump?name=${encodeURIComponent(who)}`, false);
|
||||
|
||||
@@ -24,6 +24,12 @@ import { ProcessHost } from './host.mjs';
|
||||
// (see shims/browser/build.sh), and `globalThis.GEX_ARCH` for the glue.
|
||||
const ARCH_INFIX = process.env.GEX_ARCH === 'x86' ? '.x86' : '';
|
||||
if (process.env.GEX_ARCH === 'x86') globalThis.GEX_ARCH = 'x86';
|
||||
// `GEX_MEM64=1` (with GEX_ARCH=x86 and GEX_SHARED=1): a 64-bit guest memory,
|
||||
// the `.m64` shim and seed built by `GEX_MEM64=1 shims/browser/build.sh`
|
||||
// (docs/mem64.md step 6).
|
||||
const MEM64 = !!process.env.GEX_MEM64;
|
||||
if (MEM64) globalThis.GEX_MEM64 = 1;
|
||||
const SHIM_INFIX = ARCH_INFIX + (MEM64 ? '.m64' : '');
|
||||
|
||||
// The host runs on the main thread; give it the attach-trace flag too so
|
||||
// thread spawn/adopt timing is visible from the host side, not just the Worker.
|
||||
@@ -149,7 +155,7 @@ function readStdin() {
|
||||
const SHARED = !!process.env.GEX_SHARED;
|
||||
const shimWasm = readFileSync(
|
||||
new URL(
|
||||
SHARED ? `../../../web/public/gex/gex_browser${ARCH_INFIX}.shared.wasm`
|
||||
SHARED ? `../../../web/public/gex/gex_browser${SHIM_INFIX}.shared.wasm`
|
||||
: `../../../web/public/gex/gex_browser${ARCH_INFIX}.wasm`,
|
||||
import.meta.url,
|
||||
),
|
||||
@@ -162,7 +168,7 @@ const shimWasm = readFileSync(
|
||||
let kernel = null;
|
||||
try {
|
||||
const path = new URL(
|
||||
SHARED ? `../../../web/public/gex/gex_kernel${ARCH_INFIX}.wasm`
|
||||
SHARED ? `../../../web/public/gex/gex_kernel${SHIM_INFIX}.wasm`
|
||||
: `../../../web/public/gex/gex_kernel${ARCH_INFIX}.plain.wasm`,
|
||||
import.meta.url,
|
||||
);
|
||||
@@ -267,6 +273,7 @@ const host = new ProcessHost({
|
||||
// GEX_DENSE=0: classic per-slot dispatch tables (gex_emit::dense off).
|
||||
dense: process.env.GEX_DENSE === '0' ? false : process.env.GEX_DENSE === '2' ? 2 : undefined,
|
||||
denseDebug: !!process.env.GEX_DENSE_DEBUG,
|
||||
mem64: MEM64,
|
||||
calloutTrace: !!process.env.GEX_CALLOUT_TRACE,
|
||||
inPlace: process.env.GEX_IN_PLACE === '0' ? 0 : undefined,
|
||||
excTrace: !!process.env.GEX_EXC_TRACE,
|
||||
|
||||
@@ -395,6 +395,8 @@ async function handle(msg) {
|
||||
// default -- it is one line per phase per thread. See threads-worker note.
|
||||
if (msg.attachTrace) globalThis.GEX_ATTACH_TRACE = 1;
|
||||
if (msg.denseDebug) globalThis.GEX_DENSE_DEBUG = 1;
|
||||
// A 64-bit guest memory (docs/mem64.md step 6): the wasm64 shim's process.
|
||||
if (msg.mem64) globalThis.GEX_MEM64 = 1;
|
||||
if (msg.calloutTrace) globalThis.GEX_CALLOUT_TRACE = 1;
|
||||
if (msg.lazyThread) globalThis.GEX_LAZY_THREAD = 1;
|
||||
// `kernelStack` is this Worker's slice of the kernel's stack array. Absent for
|
||||
|
||||
@@ -24,7 +24,12 @@
|
||||
// already because a module importing a shared memory has to be.
|
||||
#![cfg_attr(feature = "shared-memory", feature(stdarch_wasm_atomic_wait))]
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
use core::arch::wasm32 as warch;
|
||||
// The memory64 build (docs/mem64.md step 6): an x86-64 guest with a 64-bit
|
||||
// memory, for the Unity games on .NET (docs/unity-dotnet.md P5).
|
||||
#[cfg(target_arch = "wasm64")]
|
||||
use core::arch::wasm64 as warch;
|
||||
|
||||
use gex_abi::ctx;
|
||||
use gex_kernel::errno::{Errno, SysResult};
|
||||
|
||||
@@ -161,7 +161,7 @@ pub fn claim(kind: Kind, owner: u32, start: u64, end: u64, what: &'static str) {
|
||||
other & 0xffff_ffff
|
||||
));
|
||||
print_locked(n);
|
||||
core::arch::wasm32::unreachable();
|
||||
crate::warch::unreachable();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,5 +14,7 @@ gex-kernel.workspace = true
|
||||
[features]
|
||||
# Serve an x86-64 guest. See `gex_abi::X86`.
|
||||
x86 = ["gex-abi/x86", "gex-kernel/x86"]
|
||||
# Processes with a 64-bit memory (the wasm64 browser shim, docs/mem64.md step 6).
|
||||
process64 = ["gex-kmem/process64"]
|
||||
# Host a 32-bit big-endian PowerPC guest (docs/ppc.md).
|
||||
ppc = ["gex-abi/ppc", "gex-kernel/ppc"]
|
||||
@@ -80,6 +80,22 @@ RUSTFLAGS="-C link-arg=--import-memory \
|
||||
node tools/add-memory.mjs "$OUT" web/public/gex/gex_kernel$INFIX.wasm env process 1 1
|
||||
node tools/add-memory.mjs "$OUT" web/public/gex/gex_kernel$INFIX.plain.wasm env process 1 0
|
||||
|
||||
# GEX_MEM64=1 (x86 only): also the kernel for processes with a 64-bit memory
|
||||
# (the wasm64 shim, docs/mem64.md step 6): its process memory is a memory64 of
|
||||
# 16 GiB, and gex-kmem addresses it with i64.
|
||||
if [ -n "${GEX_MEM64:-}" ] && [ -n "$ARCH_FEATURES" ]; then
|
||||
RUSTFLAGS="-C link-arg=--import-memory \
|
||||
-C link-arg=--shared-memory \
|
||||
-C link-arg=--max-memory=4294967296 \
|
||||
-C link-arg=--export-table \
|
||||
-C link-arg=--export=__stack_pointer \
|
||||
-C target-feature=+atomics,+bulk-memory,+mutable-globals" \
|
||||
cargo +nightly build -p gex-kernel-wasm --target wasm32-unknown-unknown \
|
||||
--release -Z build-std=std,panic_abort --target-dir $TDIR-m64 --features x86,process64
|
||||
node tools/add-memory.mjs $TDIR-m64/wasm32-unknown-unknown/release/gex_kernel_wasm.wasm \
|
||||
web/public/gex/gex_kernel$INFIX.m64.wasm env process 1 1 $(((16 << 30) / 65536)) 64
|
||||
fi
|
||||
|
||||
# And the site, which is a copy of public/ and will otherwise keep serving
|
||||
# the module that was here a moment ago.
|
||||
web/refresh-dist.sh
|
||||
@@ -32,10 +32,13 @@ const [
|
||||
pages,
|
||||
shared = '1',
|
||||
maxPages = GUEST_MAX_PAGES,
|
||||
// `64`: a memory64 (limits flag 0x04), for processes with a 64-bit guest
|
||||
// memory (docs/mem64.md step 6).
|
||||
index = '32',
|
||||
] = process.argv.slice(2);
|
||||
if (!inPath || !outPath) {
|
||||
console.error(
|
||||
'usage: add-memory.mjs <in.wasm> <out.wasm> [module] [name] [min] [shared] [max]',
|
||||
'usage: add-memory.mjs <in.wasm> <out.wasm> [module] [name] [min] [shared] [max] [32|64]',
|
||||
);
|
||||
process.exit(2);
|
||||
}
|
||||
@@ -92,7 +95,7 @@ const str = (s) => [...enc(s.length), ...[...s].map((c) => c.charCodeAt(0))];
|
||||
const min = pages ? Number(pages) : GUEST_MIN_PAGES;
|
||||
const entry = [
|
||||
...str(mod), ...str(name),
|
||||
0x02, shared === '0' ? 0x01 : 0x03,
|
||||
0x02, (shared === '0' ? 0x01 : 0x03) | (index === '64' ? 0x04 : 0),
|
||||
...enc(min), ...enc(Number(maxPages)),
|
||||
];
|
||||
|
||||
|
||||
Reference in new issue
Block a user