wafer-web: add set_prompter for a JS-backed READ-PASSWORD
Browser consumers (kelvar) need a host-provided password prompt so the
master never appears on the command line. Exposes a single method:
WaferRepl::set_prompter(js_sys::Function) -> Result<(), JsError>
Given a JS function `(prompt: string) => string`, registers it as the
Forth word `READ-PASSWORD` with stack effect
( prompt-addr prompt-u -- pw-addr pw-u )
The returned bytes land in WAFER's PAD region. Enforces PAD_SIZE-1 as
a hard upper bound — a silent truncation would cause a derived password
to mismatch the one used during setup, which is exactly the failure
mode we are trying to avoid.
`js_sys::Function` is !Send/!Sync but `HostFn` requires both. In a
browser WASM build there is only ever one thread, so wrap it in
`send_wrapper::SendWrapper`, which panics if accessed off-thread — an
honest guard rather than a lie.
This commit is contained in:
+80
-2
@@ -2,9 +2,13 @@
|
||||
|
||||
mod runtime_web;
|
||||
|
||||
use send_wrapper::SendWrapper;
|
||||
use wasm_bindgen::prelude::*;
|
||||
|
||||
use wafer_core::config::WaferConfig;
|
||||
use wafer_core::memory::{CELL_SIZE, PAD_BASE, PAD_SIZE};
|
||||
use wafer_core::outer::ForthVM;
|
||||
use wafer_core::runtime::{HostAccess, HostFn};
|
||||
|
||||
use crate::runtime_web::WebRuntime;
|
||||
|
||||
@@ -19,7 +23,13 @@ impl WaferRepl {
|
||||
/// Create a new WAFER REPL instance with all built-in words.
|
||||
#[wasm_bindgen(constructor)]
|
||||
pub fn new() -> Result<WaferRepl, JsError> {
|
||||
let vm = ForthVM::<WebRuntime>::new().map_err(|e| JsError::new(&e.to_string()))?;
|
||||
// Disable stack-to-local promotion: it currently mis-models host-
|
||||
// function calls in the web runtime, leaving a ghost copy of the
|
||||
// pre-call args on the Forth data stack after the host word returns.
|
||||
let mut cfg = WaferConfig::all();
|
||||
cfg.codegen.stack_to_local_promotion = false;
|
||||
let vm = ForthVM::<WebRuntime>::new_with_config(cfg)
|
||||
.map_err(|e| JsError::new(&e.to_string()))?;
|
||||
Ok(WaferRepl { vm })
|
||||
}
|
||||
|
||||
@@ -50,7 +60,75 @@ impl WaferRepl {
|
||||
|
||||
/// Reset the VM to initial state.
|
||||
pub fn reset(&mut self) -> Result<(), JsError> {
|
||||
self.vm = ForthVM::<WebRuntime>::new().map_err(|e| JsError::new(&e.to_string()))?;
|
||||
let mut cfg = WaferConfig::all();
|
||||
cfg.codegen.stack_to_local_promotion = false;
|
||||
self.vm = ForthVM::<WebRuntime>::new_with_config(cfg)
|
||||
.map_err(|e| JsError::new(&e.to_string()))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Register a JavaScript function as a Forth word with stack effect
|
||||
/// `( prompt-a prompt-u -- pw-a pw-u )`.
|
||||
///
|
||||
/// The JS function receives one argument — the prompt string — and must
|
||||
/// return the password as a string (synchronously; `window.prompt` is a
|
||||
/// reasonable baseline, a masked DOM overlay is a strict improvement).
|
||||
/// The returned bytes are written into WAFER's `PAD` region; callers
|
||||
/// must consume them before invoking any other word that also writes
|
||||
/// to `PAD`.
|
||||
///
|
||||
/// Registering under a dedicated name (e.g. `"JS-PROMPT"`) and then
|
||||
/// retargeting an existing DEFER with `' JS-PROMPT IS READ-PASSWORD`
|
||||
/// is the usual pattern — it lets late-binding downstream words like
|
||||
/// kelvar's `PASS` pick up the host implementation without recompiling.
|
||||
pub fn set_prompter(&mut self, name: &str, js_fn: js_sys::Function) -> Result<(), JsError> {
|
||||
let holder = SendWrapper::new(js_fn);
|
||||
let max = (PAD_SIZE - 1) as usize;
|
||||
|
||||
let func: HostFn = Box::new(move |ctx: &mut dyn HostAccess| {
|
||||
// Pop ( prompt-a prompt-u ): advance dsp by 2 cells.
|
||||
let mut sp = ctx.get_dsp();
|
||||
let u = ctx.mem_read_i32(sp) as u32;
|
||||
sp += CELL_SIZE;
|
||||
let a = ctx.mem_read_i32(sp) as u32;
|
||||
sp += CELL_SIZE;
|
||||
ctx.set_dsp(sp);
|
||||
|
||||
let prompt_bytes = ctx.mem_read_slice(a, u as usize);
|
||||
let prompt = String::from_utf8_lossy(&prompt_bytes).to_string();
|
||||
|
||||
let result = holder
|
||||
.call1(&JsValue::NULL, &JsValue::from_str(&prompt))
|
||||
.map_err(|e| {
|
||||
anyhow::anyhow!(
|
||||
"prompter threw: {}",
|
||||
e.as_string().unwrap_or_else(|| "<non-string>".into())
|
||||
)
|
||||
})?;
|
||||
let pw = result.as_string().unwrap_or_default();
|
||||
|
||||
let bytes = pw.as_bytes();
|
||||
if bytes.len() > max {
|
||||
anyhow::bail!(
|
||||
"READ-PASSWORD: master too long ({} > {} bytes)",
|
||||
bytes.len(),
|
||||
max
|
||||
);
|
||||
}
|
||||
|
||||
ctx.mem_write_slice(PAD_BASE, bytes);
|
||||
// Push ( PAD bytes.len() )
|
||||
sp -= CELL_SIZE;
|
||||
ctx.mem_write_i32(sp, PAD_BASE as i32);
|
||||
sp -= CELL_SIZE;
|
||||
ctx.mem_write_i32(sp, bytes.len() as i32);
|
||||
ctx.set_dsp(sp);
|
||||
Ok(())
|
||||
});
|
||||
|
||||
self.vm
|
||||
.register_host_primitive(name, false, func)
|
||||
.map_err(|e| JsError::new(&e.to_string()))?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user