Skip to main content

bamts_node/
lib.rs

1//! Node-compatible host capabilities for BamTS.
2
3#![deny(unsafe_code)]
4
5mod timers;
6
7use std::collections::BTreeMap;
8use std::time::{Instant, SystemTime, UNIX_EPOCH};
9
10#[cfg(feature = "script-compiler")]
11use std::sync::Arc;
12
13#[cfg(feature = "script-compiler")]
14use bamts_bytecode::{Program, Verified};
15use bamts_runtime::Host;
16
17/// Classic-script compiler capability for Node hosts.
18#[cfg(feature = "script-compiler")]
19#[derive(Default)]
20pub struct ScriptCompiler;
21
22#[cfg(feature = "script-compiler")]
23impl bamts_runtime::CompileProvider for ScriptCompiler {
24    fn compile_script(
25        &mut self,
26        source: bamts_runtime::ScriptSource<'_>,
27    ) -> std::result::Result<Arc<Program<Verified>>, bamts_runtime::ScriptCompileError> {
28        bamts_compiler::compile_classic_script(
29            source.source,
30            &String::from_utf16_lossy(source.name),
31        )
32        .map(Arc::new)
33        .map_err(map_script_compile_error)
34    }
35}
36
37#[cfg(feature = "script-compiler")]
38fn map_script_compile_error(
39    error: bamts_compiler::ScriptCompileError,
40) -> bamts_runtime::ScriptCompileError {
41    match error {
42        bamts_compiler::ScriptCompileError::IllFormedSource { unit_offset } => {
43            bamts_runtime::ScriptCompileError::IllFormedSource { unit_offset }
44        }
45        bamts_compiler::ScriptCompileError::Syntax {
46            message,
47            line,
48            column,
49        } => bamts_runtime::ScriptCompileError::Syntax {
50            message,
51            line,
52            column,
53        },
54        bamts_compiler::ScriptCompileError::Unsupported {
55            message,
56            line,
57            column,
58        } => bamts_runtime::ScriptCompileError::Unsupported {
59            message,
60            line,
61            column,
62        },
63        bamts_compiler::ScriptCompileError::Capacity { message } => {
64            bamts_runtime::ScriptCompileError::Capacity { message }
65        }
66    }
67}
68
69/// Concrete Node-compatible capability state.
70///
71/// Environment and arguments are explicit rather than inherited from the
72/// embedding process, keeping executions independent of the invoking machine.
73pub struct NodeHost {
74    stdout: Vec<u8>,
75    stderr: Vec<u8>,
76    exit_code: i32,
77    argv: Vec<String>,
78    env: BTreeMap<String, String>,
79    started: Instant,
80    random_state: u64,
81    compiler: Option<Box<dyn bamts_runtime::CompileProvider>>,
82    timers: timers::NodeTimers,
83}
84
85impl Default for NodeHost {
86    fn default() -> Self {
87        Self::new()
88    }
89}
90
91impl NodeHost {
92    #[must_use]
93    pub fn new() -> Self {
94        Self {
95            stdout: Vec::new(),
96            stderr: Vec::new(),
97            exit_code: 0,
98            argv: Vec::new(),
99            env: BTreeMap::new(),
100            started: Instant::now(),
101            random_state: 0x6a09_e667_f3bc_c909,
102            compiler: None,
103            timers: timers::NodeTimers::new(),
104        }
105    }
106
107    #[must_use]
108    pub fn stdout(&self) -> &[u8] {
109        &self.stdout
110    }
111
112    #[must_use]
113    pub fn stderr(&self) -> &[u8] {
114        &self.stderr
115    }
116
117    #[must_use]
118    pub const fn exit_code(&self) -> i32 {
119        self.exit_code
120    }
121
122    pub fn set_argv(&mut self, argv: impl IntoIterator<Item = String>) {
123        self.argv = argv.into_iter().collect();
124    }
125
126    #[must_use]
127    pub fn argv(&self) -> &[String] {
128        &self.argv
129    }
130
131    #[must_use]
132    pub fn env(&self, name: &str) -> Option<&str> {
133        self.env.get(name).map(String::as_str)
134    }
135
136    pub fn set_env(&mut self, name: impl Into<String>, value: impl Into<String>) {
137        self.env.insert(name.into(), value.into());
138    }
139
140    pub fn delete_env(&mut self, name: &str) -> bool {
141        self.env.remove(name).is_some()
142    }
143
144    pub fn set_script_compiler(&mut self, compiler: Box<dyn bamts_runtime::CompileProvider>) {
145        self.compiler = Some(compiler);
146    }
147}
148
149impl Host for NodeHost {
150    fn write_stdout(&mut self, bytes: &[u8]) {
151        self.stdout.extend_from_slice(bytes);
152    }
153
154    fn write_stderr(&mut self, bytes: &[u8]) {
155        self.stderr.extend_from_slice(bytes);
156    }
157
158    fn exit_code(&self) -> i32 {
159        self.exit_code
160    }
161
162    fn set_exit_code(&mut self, exit_code: i32) {
163        self.exit_code = exit_code;
164    }
165
166    fn argv(&self) -> &[String] {
167        &self.argv
168    }
169
170    fn env(&self, name: &str) -> Option<&str> {
171        self.env.get(name).map(String::as_str)
172    }
173
174    fn set_env(&mut self, name: &str, value: &str) {
175        self.env.insert(name.to_owned(), value.to_owned());
176    }
177
178    fn delete_env(&mut self, name: &str) -> bool {
179        self.env.remove(name).is_some()
180    }
181
182    fn now_ms(&mut self) -> u64 {
183        let elapsed = SystemTime::now()
184            .duration_since(UNIX_EPOCH)
185            .unwrap_or_default();
186        u64::try_from(elapsed.as_millis()).unwrap_or(u64::MAX)
187    }
188
189    fn monotonic_ns(&mut self) -> u64 {
190        u64::try_from(self.started.elapsed().as_nanos()).unwrap_or(u64::MAX)
191    }
192
193    fn random(&mut self) -> f64 {
194        // xorshift64*: deterministic, non-cryptographic entropy for Math.random.
195        let mut state = self.random_state;
196        state ^= state >> 12;
197        state ^= state << 25;
198        state ^= state >> 27;
199        self.random_state = state;
200        let bits = state.wrapping_mul(0x2545_f491_4f6c_dd1d) >> 11;
201        (bits as f64) * (1.0 / ((1_u64 << 53) as f64))
202    }
203
204    fn script_compiler(&mut self) -> Option<&mut (dyn bamts_runtime::CompileProvider + 'static)> {
205        self.compiler.as_deref_mut()
206    }
207
208    fn timers(&mut self) -> Option<&mut (dyn bamts_runtime::TimerProvider + 'static)> {
209        Some(&mut self.timers)
210    }
211
212    fn hash(&mut self, algorithm: &str, data: &[u8]) -> Option<Vec<u8>> {
213        match algorithm.to_ascii_lowercase().replace('-', "").as_str() {
214            "sha256" => Some(sha256(data).to_vec()),
215            "sha512" => Some(sha512(data).to_vec()),
216            _ => None,
217        }
218    }
219}
220
221fn sha256(data: &[u8]) -> [u8; 32] {
222    const INITIAL: [u32; 8] = [
223        0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a, 0x510e527f, 0x9b05688c, 0x1f83d9ab,
224        0x5be0cd19,
225    ];
226    const K: [u32; 64] = [
227        0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4,
228        0xab1c5ed5, 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe,
229        0x9bdc06a7, 0xc19bf174, 0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f,
230        0x4a7484aa, 0x5cb0a9dc, 0x76f988da, 0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7,
231        0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967, 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc,
232        0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85, 0xa2bfe8a1, 0xa81a664b,
233        0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070, 0x19a4c116,
234        0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3,
235        0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7,
236        0xc67178f2,
237    ];
238    let bit_len = (data.len() as u64).wrapping_mul(8);
239    let mut padded = data.to_vec();
240    padded.push(0x80);
241    while padded.len() % 64 != 56 {
242        padded.push(0);
243    }
244    padded.extend_from_slice(&bit_len.to_be_bytes());
245    let mut state = INITIAL;
246    for block in padded.chunks_exact(64) {
247        let mut words = [0_u32; 64];
248        for (word, bytes) in words[..16].iter_mut().zip(block.chunks_exact(4)) {
249            *word = u32::from_be_bytes(bytes.try_into().expect("four bytes"));
250        }
251        for index in 16..64 {
252            let s0 = words[index - 15].rotate_right(7)
253                ^ words[index - 15].rotate_right(18)
254                ^ (words[index - 15] >> 3);
255            let s1 = words[index - 2].rotate_right(17)
256                ^ words[index - 2].rotate_right(19)
257                ^ (words[index - 2] >> 10);
258            words[index] = words[index - 16]
259                .wrapping_add(s0)
260                .wrapping_add(words[index - 7])
261                .wrapping_add(s1);
262        }
263        let [mut a, mut b, mut c, mut d, mut e, mut f, mut g, mut h] = state;
264        for index in 0..64 {
265            let s1 = e.rotate_right(6) ^ e.rotate_right(11) ^ e.rotate_right(25);
266            let choice = (e & f) ^ (!e & g);
267            let t1 = h
268                .wrapping_add(s1)
269                .wrapping_add(choice)
270                .wrapping_add(K[index])
271                .wrapping_add(words[index]);
272            let s0 = a.rotate_right(2) ^ a.rotate_right(13) ^ a.rotate_right(22);
273            let majority = (a & b) ^ (a & c) ^ (b & c);
274            let t2 = s0.wrapping_add(majority);
275            h = g;
276            g = f;
277            f = e;
278            e = d.wrapping_add(t1);
279            d = c;
280            c = b;
281            b = a;
282            a = t1.wrapping_add(t2);
283        }
284        for (slot, value) in state.iter_mut().zip([a, b, c, d, e, f, g, h]) {
285            *slot = slot.wrapping_add(value);
286        }
287    }
288    let mut digest = [0_u8; 32];
289    for (chunk, value) in digest.chunks_exact_mut(4).zip(state) {
290        chunk.copy_from_slice(&value.to_be_bytes());
291    }
292    digest
293}
294
295fn sha512(data: &[u8]) -> [u8; 64] {
296    const INITIAL: [u64; 8] = [
297        0x6a09e667f3bcc908,
298        0xbb67ae8584caa73b,
299        0x3c6ef372fe94f82b,
300        0xa54ff53a5f1d36f1,
301        0x510e527fade682d1,
302        0x9b05688c2b3e6c1f,
303        0x1f83d9abfb41bd6b,
304        0x5be0cd19137e2179,
305    ];
306    const K: [u64; 80] = [
307        0x428a2f98d728ae22,
308        0x7137449123ef65cd,
309        0xb5c0fbcfec4d3b2f,
310        0xe9b5dba58189dbbc,
311        0x3956c25bf348b538,
312        0x59f111f1b605d019,
313        0x923f82a4af194f9b,
314        0xab1c5ed5da6d8118,
315        0xd807aa98a3030242,
316        0x12835b0145706fbe,
317        0x243185be4ee4b28c,
318        0x550c7dc3d5ffb4e2,
319        0x72be5d74f27b896f,
320        0x80deb1fe3b1696b1,
321        0x9bdc06a725c71235,
322        0xc19bf174cf692694,
323        0xe49b69c19ef14ad2,
324        0xefbe4786384f25e3,
325        0x0fc19dc68b8cd5b5,
326        0x240ca1cc77ac9c65,
327        0x2de92c6f592b0275,
328        0x4a7484aa6ea6e483,
329        0x5cb0a9dcbd41fbd4,
330        0x76f988da831153b5,
331        0x983e5152ee66dfab,
332        0xa831c66d2db43210,
333        0xb00327c898fb213f,
334        0xbf597fc7beef0ee4,
335        0xc6e00bf33da88fc2,
336        0xd5a79147930aa725,
337        0x06ca6351e003826f,
338        0x142929670a0e6e70,
339        0x27b70a8546d22ffc,
340        0x2e1b21385c26c926,
341        0x4d2c6dfc5ac42aed,
342        0x53380d139d95b3df,
343        0x650a73548baf63de,
344        0x766a0abb3c77b2a8,
345        0x81c2c92e47edaee6,
346        0x92722c851482353b,
347        0xa2bfe8a14cf10364,
348        0xa81a664bbc423001,
349        0xc24b8b70d0f89791,
350        0xc76c51a30654be30,
351        0xd192e819d6ef5218,
352        0xd69906245565a910,
353        0xf40e35855771202a,
354        0x106aa07032bbd1b8,
355        0x19a4c116b8d2d0c8,
356        0x1e376c085141ab53,
357        0x2748774cdf8eeb99,
358        0x34b0bcb5e19b48a8,
359        0x391c0cb3c5c95a63,
360        0x4ed8aa4ae3418acb,
361        0x5b9cca4f7763e373,
362        0x682e6ff3d6b2b8a3,
363        0x748f82ee5defb2fc,
364        0x78a5636f43172f60,
365        0x84c87814a1f0ab72,
366        0x8cc702081a6439ec,
367        0x90befffa23631e28,
368        0xa4506cebde82bde9,
369        0xbef9a3f7b2c67915,
370        0xc67178f2e372532b,
371        0xca273eceea26619c,
372        0xd186b8c721c0c207,
373        0xeada7dd6cde0eb1e,
374        0xf57d4f7fee6ed178,
375        0x06f067aa72176fba,
376        0x0a637dc5a2c898a6,
377        0x113f9804bef90dae,
378        0x1b710b35131c471b,
379        0x28db77f523047d84,
380        0x32caab7b40c72493,
381        0x3c9ebe0a15c9bebc,
382        0x431d67c49c100d4c,
383        0x4cc5d4becb3e42b6,
384        0x597f299cfc657e2a,
385        0x5fcb6fab3ad6faec,
386        0x6c44198c4a475817,
387    ];
388    let bit_len = (data.len() as u128).wrapping_mul(8);
389    let mut padded = data.to_vec();
390    padded.push(0x80);
391    while padded.len() % 128 != 112 {
392        padded.push(0);
393    }
394    padded.extend_from_slice(&bit_len.to_be_bytes());
395    let mut state = INITIAL;
396    for block in padded.chunks_exact(128) {
397        let mut words = [0_u64; 80];
398        for (word, bytes) in words[..16].iter_mut().zip(block.chunks_exact(8)) {
399            *word = u64::from_be_bytes(bytes.try_into().expect("eight bytes"));
400        }
401        for index in 16..80 {
402            let s0 = words[index - 15].rotate_right(1)
403                ^ words[index - 15].rotate_right(8)
404                ^ (words[index - 15] >> 7);
405            let s1 = words[index - 2].rotate_right(19)
406                ^ words[index - 2].rotate_right(61)
407                ^ (words[index - 2] >> 6);
408            words[index] = words[index - 16]
409                .wrapping_add(s0)
410                .wrapping_add(words[index - 7])
411                .wrapping_add(s1);
412        }
413        let [mut a, mut b, mut c, mut d, mut e, mut f, mut g, mut h] = state;
414        for index in 0..80 {
415            let s1 = e.rotate_right(14) ^ e.rotate_right(18) ^ e.rotate_right(41);
416            let choice = (e & f) ^ (!e & g);
417            let t1 = h
418                .wrapping_add(s1)
419                .wrapping_add(choice)
420                .wrapping_add(K[index])
421                .wrapping_add(words[index]);
422            let s0 = a.rotate_right(28) ^ a.rotate_right(34) ^ a.rotate_right(39);
423            let majority = (a & b) ^ (a & c) ^ (b & c);
424            let t2 = s0.wrapping_add(majority);
425            h = g;
426            g = f;
427            f = e;
428            e = d.wrapping_add(t1);
429            d = c;
430            c = b;
431            b = a;
432            a = t1.wrapping_add(t2);
433        }
434        for (slot, value) in state.iter_mut().zip([a, b, c, d, e, f, g, h]) {
435            *slot = slot.wrapping_add(value);
436        }
437    }
438    let mut digest = [0_u8; 64];
439    for (chunk, value) in digest.chunks_exact_mut(8).zip(state) {
440        chunk.copy_from_slice(&value.to_be_bytes());
441    }
442    digest
443}
444
445#[cfg(feature = "aot-main")]
446fn decode_aot_program(
447    bytes: &[u8],
448) -> Result<bamts_bytecode::Program<bamts_bytecode::Verified>, bamts_bytecode::ProgramLoadError> {
449    bamts_bytecode::decode_verified_program(bytes, &bamts_bytecode::ProgramDecodeLimits::default())
450}
451
452#[cfg(all(feature = "aot-main", not(test)))]
453fn run_aot_main() -> i32 {
454    use bamts_native::linked_program;
455    use bamts_runtime::{Limits, run_linked_program};
456
457    let mut host = NodeHost::new();
458    #[cfg(feature = "script-compiler")]
459    host.set_script_compiler(Box::new(ScriptCompiler));
460    let linked = match linked_program() {
461        Ok(linked) => linked,
462        Err(_) => return finish_aot_process(&host, AotCompletion::Failure(AotMainFailure::Link)),
463    };
464    let program = match decode_aot_program(linked.bytecode()) {
465        Ok(program) => program,
466        Err(_) => return finish_aot_process(&host, AotCompletion::Failure(AotMainFailure::Decode)),
467    };
468    if let Err(error) =
469        initialize_aot_process_context(&mut host, std::env::args_os(), std::env::vars_os())
470    {
471        return finish_aot_process(
472            &host,
473            AotCompletion::Failure(AotMainFailure::Context(error)),
474        );
475    }
476    let outcome = match run_linked_program(&program, &linked, &mut host, &Limits::default()) {
477        Ok(outcome) => outcome,
478        Err(_) => {
479            return finish_aot_process(&host, AotCompletion::Failure(AotMainFailure::Runtime));
480        }
481    };
482    finish_aot_process(&host, AotCompletion::Success(&outcome))
483}
484
485#[cfg(any(feature = "aot-main", test))]
486#[derive(Debug, Clone, Copy, PartialEq, Eq)]
487enum AotMainFailure {
488    Link,
489    Decode,
490    Context(AotProcessContextError),
491    Runtime,
492}
493
494#[cfg(any(feature = "aot-main", test))]
495impl std::fmt::Display for AotMainFailure {
496    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
497        match self {
498            Self::Link => formatter.write_str("aot link"),
499            Self::Decode => formatter.write_str("aot decode"),
500            Self::Context(AotProcessContextError::Argument) => {
501                formatter.write_str("aot context argument")
502            }
503            Self::Context(AotProcessContextError::EnvironmentName) => {
504                formatter.write_str("aot context environment name")
505            }
506            Self::Context(AotProcessContextError::EnvironmentValue) => {
507                formatter.write_str("aot context environment value")
508            }
509            Self::Runtime => formatter.write_str("aot runtime"),
510        }
511    }
512}
513
514#[cfg(any(feature = "aot-main", test))]
515enum AotCompletion<'a> {
516    Success(&'a bamts_runtime::ExecutionOutcome),
517    Failure(AotMainFailure),
518}
519
520/// Emits buffered host stdout only on success and always flushes host stderr.
521#[cfg(any(feature = "aot-main", test))]
522fn write_aot_completion(
523    host: &NodeHost,
524    completion: AotCompletion<'_>,
525    stdout: &mut impl std::io::Write,
526    stderr: &mut impl std::io::Write,
527) -> std::io::Result<i32> {
528    let (exit_code, failure) = match completion {
529        AotCompletion::Success(outcome) => {
530            stdout.write_all(host.stdout())?;
531            stdout.write_all(&outcome.stdout)?;
532            (
533                if host.exit_code() == 0 {
534                    outcome.exit_code
535                } else {
536                    host.exit_code()
537                },
538                None,
539            )
540        }
541        AotCompletion::Failure(error) => (1, Some(error)),
542    };
543    stderr.write_all(host.stderr())?;
544    if let Some(error) = failure {
545        writeln!(stderr, "bamts: {error}")?;
546    }
547    stderr.flush()?;
548    Ok(exit_code)
549}
550
551#[cfg(all(feature = "aot-main", not(test)))]
552fn finish_aot_process(host: &NodeHost, completion: AotCompletion<'_>) -> i32 {
553    let mut stdout = std::io::stdout().lock();
554    let mut stderr = std::io::stderr().lock();
555    write_aot_completion(host, completion, &mut stdout, &mut stderr).unwrap_or(1)
556}
557
558/// C process entry for a linked BamTS AOT image.
559#[cfg(all(feature = "aot-main", not(test)))]
560#[allow(unsafe_code)]
561#[unsafe(no_mangle)]
562pub extern "C" fn main() -> i32 {
563    run_aot_main()
564}
565
566#[cfg(any(feature = "aot-main", test))]
567#[derive(Debug, Clone, Copy, PartialEq, Eq)]
568enum AotProcessContextError {
569    Argument,
570    EnvironmentName,
571    EnvironmentValue,
572}
573
574/// Populate an AOT host from an explicit process snapshot.
575///
576/// The leading `bamts` mirrors the JIT driver's argv convention; the AOT
577/// executable path occupies the entrypoint slot. Conversion is all-or-nothing
578/// so an invalid OS string cannot leave a partially populated host.
579#[cfg(any(feature = "aot-main", test))]
580fn initialize_aot_process_context(
581    host: &mut NodeHost,
582    args: impl IntoIterator<Item = std::ffi::OsString>,
583    environment: impl IntoIterator<Item = (std::ffi::OsString, std::ffi::OsString)>,
584) -> Result<(), AotProcessContextError> {
585    let mut argv = vec!["bamts".to_owned()];
586    for argument in args {
587        argv.push(
588            argument
589                .into_string()
590                .map_err(|_| AotProcessContextError::Argument)?,
591        );
592    }
593
594    let mut env = BTreeMap::new();
595    for (name, value) in environment {
596        let name = name
597            .into_string()
598            .map_err(|_| AotProcessContextError::EnvironmentName)?;
599        let value = value
600            .into_string()
601            .map_err(|_| AotProcessContextError::EnvironmentValue)?;
602        env.insert(name, value);
603    }
604
605    host.argv = argv;
606    host.env = env;
607    Ok(())
608}
609
610#[cfg(test)]
611mod tests {
612    use super::*;
613    #[cfg(feature = "aot-main")]
614    #[test]
615    fn linked_descriptor_decodes_whole_program_and_tuple_entry() {
616        use bamts_bytecode::{
617            Constant, ConstantId, EcmaString, Function, FunctionFlags, FunctionId, Instruction,
618            Module, ModuleId, Program, ProgramModule,
619        };
620
621        let module = |name: &str| ProgramModule {
622            name: ConstantId::new(0),
623            code: Module::new(
624                vec![Constant::String(EcmaString::from_utf8(name))],
625                vec![Function::new(
626                    None,
627                    0,
628                    0,
629                    0,
630                    FunctionFlags::default(),
631                    vec![Instruction::Halt],
632                    Vec::new(),
633                )],
634                FunctionId::new(0),
635            )
636            .verify()
637            .expect("descriptor test module verifies"),
638            edges: Vec::new(),
639            bindings: Vec::new(),
640            exports: Vec::new(),
641        };
642        let program = Program::link(
643            vec![module("dependency"), module("entry")],
644            ModuleId::new(1),
645        )
646        .expect("descriptor test program links");
647
648        let decoded = decode_aot_program(&program.encode()).expect("descriptor program decodes");
649
650        assert_eq!(decoded.entry(), ModuleId::new(1));
651        assert_eq!(decoded.modules().len(), 2);
652        assert!(
653            decoded
654                .modules()
655                .iter()
656                .all(|module| module.code().entry() == FunctionId::new(0))
657        );
658    }
659
660    fn hex(bytes: &[u8]) -> String {
661        const DIGITS: &[u8; 16] = b"0123456789abcdef";
662        let mut text = String::with_capacity(bytes.len() * 2);
663        for byte in bytes {
664            text.push(char::from(DIGITS[usize::from(byte >> 4)]));
665            text.push(char::from(DIGITS[usize::from(byte & 0x0f)]));
666        }
667        text
668    }
669
670    #[test]
671    fn capabilities_capture_bytes_and_mutate_process_state() {
672        let mut host = NodeHost::new();
673        Host::write_stdout(&mut host, b"out");
674        Host::write_stderr(&mut host, b"err");
675        Host::set_exit_code(&mut host, 23);
676        host.set_argv(["bamts".to_owned(), "file.ts".to_owned()]);
677        Host::set_env(&mut host, "NODE_ENV", "test");
678        assert_eq!(host.stdout(), b"out");
679        assert_eq!(host.stderr(), b"err");
680        assert_eq!(host.exit_code(), 23);
681        assert_eq!(host.argv(), ["bamts", "file.ts"]);
682        assert_eq!(host.env("NODE_ENV"), Some("test"));
683        assert!(Host::delete_env(&mut host, "NODE_ENV"));
684        assert_eq!(host.env("NODE_ENV"), None);
685    }
686
687    #[test]
688    fn sha2_matches_standard_vectors() {
689        let mut host = NodeHost::new();
690        assert_eq!(
691            hex(&Host::hash(&mut host, "sha-256", b"abc").unwrap()),
692            "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad"
693        );
694        assert_eq!(
695            hex(&Host::hash(&mut host, "SHA512", b"abc").unwrap()),
696            "ddaf35a193617abacc417349ae20413112e6fa4e89a97ea20a9eeee64b55d39a2192992a274fc1a836ba3c23a3feebbd454d4423643ce80e2a9ac94fa54ca49f"
697        );
698        assert_eq!(Host::hash(&mut host, "md5", b"abc"), None);
699    }
700
701    #[test]
702    fn aot_process_context_uses_jit_argv_normalization() {
703        let mut host = NodeHost::new();
704        initialize_aot_process_context(
705            &mut host,
706            [
707                std::ffi::OsString::from("/tmp/program"),
708                std::ffi::OsString::from("--flag"),
709            ],
710            [
711                (
712                    std::ffi::OsString::from("ZED"),
713                    std::ffi::OsString::from("last"),
714                ),
715                (
716                    std::ffi::OsString::from("ALPHA"),
717                    std::ffi::OsString::from("first"),
718                ),
719            ],
720        )
721        .unwrap();
722
723        assert_eq!(host.argv(), ["bamts", "/tmp/program", "--flag"]);
724        assert_eq!(host.env("ALPHA"), Some("first"));
725        assert_eq!(host.env("ZED"), Some("last"));
726    }
727
728    #[test]
729    fn aot_runtime_failure_emits_host_stderr_and_stable_error() {
730        let mut host = NodeHost::new();
731        Host::write_stdout(&mut host, b"before failure");
732        Host::write_stderr(&mut host, b"host diagnostic\n");
733        let mut stdout = Vec::new();
734        let mut stderr = Vec::new();
735
736        let exit_code = write_aot_completion(
737            &host,
738            AotCompletion::Failure(AotMainFailure::Runtime),
739            &mut stdout,
740            &mut stderr,
741        )
742        .expect("completion writes");
743
744        assert_eq!(exit_code, 1);
745        assert!(stdout.is_empty());
746        assert_eq!(stderr, b"host diagnostic\nbamts: aot runtime\n");
747    }
748
749    #[test]
750    fn aot_failure_labels_are_stable() {
751        assert_eq!(AotMainFailure::Link.to_string(), "aot link");
752        assert_eq!(AotMainFailure::Decode.to_string(), "aot decode");
753        assert_eq!(
754            AotMainFailure::Context(AotProcessContextError::Argument).to_string(),
755            "aot context argument"
756        );
757    }
758
759    #[test]
760    fn aot_success_preserves_host_and_runtime_output_and_exit_precedence() {
761        let mut host = NodeHost::new();
762        Host::write_stdout(&mut host, b"host stdout");
763        Host::write_stderr(&mut host, b"host stderr");
764        let outcome = bamts_runtime::ExecutionOutcome {
765            stdout: b"runtime stdout".to_vec(),
766            exit_code: 7,
767        };
768        let mut stdout = Vec::new();
769        let mut stderr = Vec::new();
770
771        let exit_code = write_aot_completion(
772            &host,
773            AotCompletion::Success(&outcome),
774            &mut stdout,
775            &mut stderr,
776        )
777        .expect("completion writes");
778
779        assert_eq!(exit_code, 7);
780        assert_eq!(stdout, b"host stdoutruntime stdout");
781        assert_eq!(stderr, b"host stderr");
782
783        Host::set_exit_code(&mut host, 11);
784        let mut stdout = Vec::new();
785        let mut stderr = Vec::new();
786        let exit_code = write_aot_completion(
787            &host,
788            AotCompletion::Success(&outcome),
789            &mut stdout,
790            &mut stderr,
791        )
792        .expect("completion writes");
793
794        assert_eq!(exit_code, 11);
795        assert_eq!(stdout, b"host stdoutruntime stdout");
796        assert_eq!(stderr, b"host stderr");
797    }
798
799    #[cfg(unix)]
800    #[test]
801    fn aot_process_context_rejects_non_unicode_without_mutating_host() {
802        use std::os::unix::ffi::OsStringExt;
803
804        let mut host = NodeHost::new();
805        let error = initialize_aot_process_context(
806            &mut host,
807            [std::ffi::OsString::from_vec(vec![0xff])],
808            [(
809                std::ffi::OsString::from("SAFE"),
810                std::ffi::OsString::from("value"),
811            )],
812        )
813        .unwrap_err();
814
815        assert_eq!(error, AotProcessContextError::Argument);
816        assert!(host.argv().is_empty());
817        assert_eq!(host.env("SAFE"), None);
818    }
819
820    #[test]
821    fn clocks_and_random_are_capabilities() {
822        let mut host = NodeHost::new();
823        assert!(Host::now_ms(&mut host) > 0);
824        let first = Host::monotonic_ns(&mut host);
825        let second = Host::monotonic_ns(&mut host);
826        assert!(second >= first);
827        let random = Host::random(&mut host);
828        assert!((0.0..1.0).contains(&random));
829    }
830
831    #[cfg(unix)]
832    #[test]
833    fn aot_process_context_rejects_non_unicode_environment_without_mutating_host() {
834        use std::os::unix::ffi::OsStringExt;
835
836        let mut host = NodeHost::new();
837        let error = initialize_aot_process_context(
838            &mut host,
839            [std::ffi::OsString::from("/tmp/program")],
840            [(
841                std::ffi::OsString::from("SAFE"),
842                std::ffi::OsString::from_vec(vec![0xff]),
843            )],
844        )
845        .unwrap_err();
846
847        assert_eq!(error, AotProcessContextError::EnvironmentValue);
848        assert!(host.argv().is_empty());
849        assert_eq!(host.env("SAFE"), None);
850    }
851}
852
853#[cfg(test)]
854mod timer_tests {
855    use super::*;
856    use std::time::Duration;
857
858    #[test]
859    fn real_timers_expire_in_deadline_order_through_one_delay_queue() {
860        let mut host = NodeHost::new();
861        let timers = Host::timers(&mut host).expect("timer capability is always present");
862
863        // The later-deadline timer is scheduled first to prove ordering comes
864        // from the deadline, not insertion order.
865        let late = timers.schedule(1, 12).expect("schedule id 1");
866        let early = timers.schedule(2, 4).expect("schedule id 2");
867        assert!(early <= late, "smaller delay yields an earlier deadline");
868        assert!(timers.has_pending());
869
870        let first = timers.wait_expired().expect("wait").expect("a wakeup");
871        let second = timers.wait_expired().expect("wait").expect("a wakeup");
872        assert_eq!(first.id, 2, "the earlier deadline fires first");
873        assert_eq!(second.id, 1);
874        assert_eq!(first.deadline_ms, early);
875        assert_eq!(second.deadline_ms, late);
876
877        assert!(!timers.has_pending());
878        assert!(
879            timers.wait_expired().expect("wait").is_none(),
880            "an empty pending set never blocks"
881        );
882    }
883
884    #[test]
885    fn cancellation_removes_exactly_the_target_id() {
886        let mut host = NodeHost::new();
887        let timers = Host::timers(&mut host).unwrap();
888
889        timers.schedule(10, 20).expect("schedule id 10");
890        timers.schedule(11, 4).expect("schedule id 11");
891
892        assert!(timers.cancel(10).expect("cancel"), "an armed timer cancels");
893        assert!(
894            !timers.cancel(10).expect("cancel"),
895            "a second cancel of the same id is false"
896        );
897        assert!(
898            !timers.cancel(999).expect("cancel"),
899            "an unknown id cancels to false"
900        );
901
902        let wakeup = timers.wait_expired().expect("wait").expect("a wakeup");
903        assert_eq!(wakeup.id, 11, "only the surviving timer fires");
904        assert!(!timers.has_pending());
905        assert!(timers.wait_expired().expect("wait").is_none());
906    }
907
908    #[test]
909    fn expiry_that_races_a_cancel_is_dropped_as_stale() {
910        let mut host = NodeHost::new();
911        let timers = Host::timers(&mut host).unwrap();
912
913        timers.schedule(7, 1).expect("schedule id 7");
914        // Let the worker fire and queue the wakeup while the caller has not yet
915        // polled it, then cancel: the caller-side pending set is authoritative.
916        std::thread::sleep(Duration::from_millis(40));
917        assert!(
918            timers.cancel(7).expect("cancel"),
919            "still pending from the caller's view until polled"
920        );
921
922        let mut output = Vec::new();
923        timers.poll_expired(&mut output).expect("poll");
924        assert!(output.is_empty(), "a cancelled id is never delivered");
925        assert!(!timers.has_pending());
926        assert!(timers.wait_expired().expect("wait").is_none());
927    }
928
929    #[test]
930    fn worker_is_lazy_and_shuts_down_cleanly_on_drop() {
931        let mut host = NodeHost::new();
932        assert!(
933            !host.timers.worker_active(),
934            "no worker thread before the first schedule"
935        );
936
937        // Merely returning the capability must not spawn the worker.
938        let _ = Host::timers(&mut host);
939        assert!(
940            !host.timers.worker_active(),
941            "returning the capability is not a schedule"
942        );
943
944        Host::timers(&mut host)
945            .unwrap()
946            .schedule(1, 1)
947            .expect("schedule id 1");
948        assert!(
949            host.timers.worker_active(),
950            "the worker starts lazily on first schedule"
951        );
952
953        // Dropping the host drops the worker handle, which closes the command
954        // channel and joins the thread even with a still-armed timer. A hang or
955        // panic here fails the test.
956        drop(host);
957    }
958
959    #[test]
960    fn constructs_and_runs_inside_an_ambient_tokio_runtime_without_panic() {
961        let runtime = tokio::runtime::Builder::new_current_thread()
962            .enable_time()
963            .build()
964            .expect("ambient runtime builds");
965        let _guard = runtime.enter();
966
967        // With an ambient Tokio runtime on this thread, the provider must still
968        // spawn its own dedicated worker thread/runtime and never `block_on`
969        // here, so none of these operations panic.
970        let mut host = NodeHost::new();
971        let timers = Host::timers(&mut host).unwrap();
972        let deadline = timers
973            .schedule(1, 2)
974            .expect("schedule under ambient runtime");
975        assert!(deadline >= 2);
976        let wakeup = timers.wait_expired().expect("wait").expect("a wakeup");
977        assert_eq!(wakeup.id, 1);
978        assert!(!timers.has_pending());
979    }
980}