22
33use std:: sync:: { Arc , Mutex } ;
44
5- use crate :: bridge:: external_refs;
5+ use crate :: bridge:: { external_refs, register_stub_bridge_fns } ;
66use crate :: isolate:: init_v8_platform;
7+ use crate :: session:: { SYNC_BRIDGE_FNS , ASYNC_BRIDGE_FNS } ;
78
89/// Maximum allowed snapshot blob size (50MB).
910/// Prevents resource exhaustion from degenerate bridge code.
1011const MAX_SNAPSHOT_BLOB_BYTES : usize = 50 * 1024 * 1024 ;
1112
12- /// Create a V8 startup snapshot with the given bridge code pre-compiled .
13+ /// Create a V8 startup snapshot with a fully-initialized bridge context .
1314///
14- /// Consumes a temporary isolate. The returned StartupData contains the
15- /// serialized V8 heap with compiled bytecode.
15+ /// Registers stub bridge functions on the global, injects default config
16+ /// globals, then compiles and executes the bridge IIFE. The resulting
17+ /// context — with all bridge infrastructure set up — is snapshotted.
18+ ///
19+ /// After restore, stub bridge functions are replaced with real session-local
20+ /// ones, and per-session config is injected via a post-restore script.
1621///
1722/// Returns an error if the bridge code fails to compile or the resulting
1823/// snapshot exceeds MAX_SNAPSHOT_BLOB_BYTES.
@@ -25,7 +30,13 @@ pub fn create_snapshot(bridge_code: &str) -> Result<v8::StartupData, String> {
2530 let context = v8:: Context :: new ( scope, Default :: default ( ) ) ;
2631 let scope = & mut v8:: ContextScope :: new ( scope, context) ;
2732
28- // Compile and run bridge code — bytecode is captured in snapshot
33+ // Register stub bridge functions so the IIFE can reference them
34+ register_stub_bridge_fns ( scope, & SYNC_BRIDGE_FNS , & ASYNC_BRIDGE_FNS ) ;
35+
36+ // Inject default config globals for bridge IIFE setup
37+ inject_snapshot_defaults ( scope) ;
38+
39+ // Compile and run bridge code — context captures fully-initialized state
2940 let source = v8:: String :: new ( scope, bridge_code)
3041 . ok_or_else ( || "failed to create V8 string for bridge code" . to_string ( ) ) ?;
3142 let script = v8:: Script :: compile ( scope, source, None )
@@ -50,6 +61,50 @@ pub fn create_snapshot(bridge_code: &str) -> Result<v8::StartupData, String> {
5061 Ok ( blob)
5162}
5263
64+ /// Inject default config globals needed by the bridge IIFE during snapshot creation.
65+ ///
66+ /// These are placeholder values so bridge code that reads _processConfig or
67+ /// _osConfig at setup time doesn't fail. They're overwritten per-session
68+ /// after snapshot restore via inject_globals_from_payload.
69+ fn inject_snapshot_defaults ( scope : & mut v8:: HandleScope ) {
70+ let context = scope. get_current_context ( ) ;
71+ let global = context. global ( scope) ;
72+
73+ // _processConfig: default placeholder (overwritten per-session)
74+ let pc_code = r#"({
75+ cwd: "/",
76+ env: {},
77+ timing_mitigation: "off",
78+ frozen_time_ms: null
79+ })"# ;
80+ let pc_source = v8:: String :: new ( scope, pc_code) . unwrap ( ) ;
81+ let pc_script = v8:: Script :: compile ( scope, pc_source, None ) . unwrap ( ) ;
82+ let pc_val = pc_script. run ( scope) . unwrap ( ) ;
83+ if let Some ( pc_obj) = pc_val. to_object ( scope) {
84+ pc_obj. set_integrity_level ( scope, v8:: IntegrityLevel :: Frozen ) ;
85+ }
86+ let pc_key = v8:: String :: new ( scope, "_processConfig" ) . unwrap ( ) ;
87+ let attr = v8:: PropertyAttribute :: READ_ONLY | v8:: PropertyAttribute :: DONT_DELETE ;
88+ global. define_own_property ( scope, pc_key. into ( ) , pc_val, attr) ;
89+
90+ // _osConfig: default placeholder (overwritten per-session)
91+ let oc_code = r#"({
92+ homedir: "/root",
93+ tmpdir: "/tmp",
94+ platform: "linux",
95+ arch: "x64"
96+ })"# ;
97+ let oc_source = v8:: String :: new ( scope, oc_code) . unwrap ( ) ;
98+ let oc_script = v8:: Script :: compile ( scope, oc_source, None ) . unwrap ( ) ;
99+ let oc_val = oc_script. run ( scope) . unwrap ( ) ;
100+ if let Some ( oc_obj) = oc_val. to_object ( scope) {
101+ oc_obj. set_integrity_level ( scope, v8:: IntegrityLevel :: Frozen ) ;
102+ }
103+ let oc_key = v8:: String :: new ( scope, "_osConfig" ) . unwrap ( ) ;
104+ let attr2 = v8:: PropertyAttribute :: READ_ONLY | v8:: PropertyAttribute :: DONT_DELETE ;
105+ global. define_own_property ( scope, oc_key. into ( ) , oc_val, attr2) ;
106+ }
107+
53108/// Create a V8 isolate restored from a snapshot blob.
54109///
55110/// The external references must match those used during snapshot creation
@@ -519,7 +574,6 @@ mod tests {
519574 // resulting context can be snapshotted.
520575 {
521576 use crate :: bridge:: register_stub_bridge_fns;
522- use crate :: session:: { SYNC_BRIDGE_FNS , ASYNC_BRIDGE_FNS } ;
523577
524578 let mut snapshot_isolate = v8:: Isolate :: snapshot_creator ( Some ( external_refs ( ) ) , None ) ;
525579 {
@@ -605,5 +659,168 @@ mod tests {
605659 ) ;
606660 assert ! ( blob. unwrap( ) . len( ) > 0 , "snapshot blob should be non-empty" ) ;
607661 }
662+
663+ // --- Part 15: create_snapshot() auto-registers stubs and injects defaults ---
664+ // Verifies that create_snapshot() registers all bridge function stubs
665+ // and injects _processConfig/_osConfig defaults before running bridge code.
666+ {
667+ // Bridge IIFE that verifies stubs and config globals exist
668+ let iife_code = r#"
669+ (function() {
670+ // Verify all sync bridge functions are registered as stubs
671+ var syncFns = ['_log', '_error', '_resolveModule', '_loadFile',
672+ '_loadPolyfill', '_cryptoRandomFill', '_cryptoRandomUUID',
673+ '_fsReadFile', '_fsWriteFile', '_fsReadFileBinary',
674+ '_fsWriteFileBinary', '_fsReadDir', '_fsMkdir', '_fsRmdir',
675+ '_fsExists', '_fsStat', '_fsUnlink', '_fsRename', '_fsChmod',
676+ '_fsChown', '_fsLink', '_fsSymlink', '_fsReadlink', '_fsLstat',
677+ '_fsTruncate', '_fsUtimes', '_childProcessSpawnStart',
678+ '_childProcessStdinWrite', '_childProcessStdinClose',
679+ '_childProcessKill', '_childProcessSpawnSync'];
680+ for (var i = 0; i < syncFns.length; i++) {
681+ if (typeof globalThis[syncFns[i]] !== 'function') {
682+ throw new Error('Missing sync stub: ' + syncFns[i] +
683+ ' (typeof=' + typeof globalThis[syncFns[i]] + ')');
684+ }
685+ }
686+
687+ // Verify all async bridge functions are registered as stubs
688+ var asyncFns = ['_dynamicImport', '_scheduleTimer',
689+ '_networkFetchRaw', '_networkDnsLookupRaw',
690+ '_networkHttpRequestRaw', '_networkHttpServerListenRaw',
691+ '_networkHttpServerCloseRaw'];
692+ for (var i = 0; i < asyncFns.length; i++) {
693+ if (typeof globalThis[asyncFns[i]] !== 'function') {
694+ throw new Error('Missing async stub: ' + asyncFns[i] +
695+ ' (typeof=' + typeof globalThis[asyncFns[i]] + ')');
696+ }
697+ }
698+
699+ // Verify _processConfig default was injected
700+ if (typeof _processConfig !== 'object' || _processConfig === null) {
701+ throw new Error('_processConfig not injected: ' + typeof _processConfig);
702+ }
703+ if (_processConfig.cwd !== '/') {
704+ throw new Error('_processConfig.cwd should be "/", got: ' + _processConfig.cwd);
705+ }
706+
707+ // Verify _osConfig default was injected
708+ if (typeof _osConfig !== 'object' || _osConfig === null) {
709+ throw new Error('_osConfig not injected: ' + typeof _osConfig);
710+ }
711+ if (_osConfig.platform !== 'linux') {
712+ throw new Error('_osConfig.platform should be "linux", got: ' + _osConfig.platform);
713+ }
714+
715+ globalThis.__part15_ok = true;
716+ })();
717+ "# ;
718+ let blob = create_snapshot ( iife_code) . expect (
719+ "create_snapshot should succeed with bridge code that checks stubs and defaults"
720+ ) ;
721+ assert ! ( blob. len( ) > 0 , "snapshot blob should be non-empty" ) ;
722+
723+ // Verify the snapshot can be restored
724+ let mut isolate = create_isolate_from_snapshot ( blob, None ) ;
725+ assert_eq ! ( eval( & mut isolate, "1 + 1" ) , "2" ) ;
726+ }
727+
728+ // --- Part 16: create_snapshot() with getter facade and closures ---
729+ // Verifies that the full bridge pattern (stubs, closures, getter facade,
730+ // config globals) works through create_snapshot() and the context is
731+ // correctly snapshotted via set_default_context.
732+ {
733+ let iife_code = r#"
734+ (function() {
735+ // Set up getter-based fs facade referencing bridge stubs
736+ var _fs = {};
737+ Object.defineProperties(_fs, {
738+ readFile: { get: function() { return globalThis._fsReadFile; }, enumerable: true },
739+ writeFile: { get: function() { return globalThis._fsWriteFile; }, enumerable: true },
740+ });
741+ globalThis._fs = _fs;
742+
743+ // Set up closure wrapping a bridge stub
744+ globalThis.myLog = function() {
745+ return globalThis._log.apply(null, arguments);
746+ };
747+
748+ // Set up a require-like function (doesn't call _loadPolyfill at setup)
749+ globalThis.require = function(name) {
750+ return globalThis._loadPolyfill(name);
751+ };
752+
753+ // Set up a console-like object
754+ globalThis.console = {
755+ log: function() { globalThis._log.apply(null, arguments); },
756+ error: function() { globalThis._error.apply(null, arguments); },
757+ };
758+
759+ // Read _processConfig at setup time (like process.cwd initialization)
760+ globalThis.__initialCwd = _processConfig.cwd;
761+
762+ globalThis.__part16_setup = true;
763+ })();
764+ "# ;
765+ let blob = create_snapshot ( iife_code) . expect (
766+ "create_snapshot should succeed with full bridge IIFE pattern"
767+ ) ;
768+ assert ! ( blob. len( ) > 0 ) ;
769+
770+ // Restore and verify default context has the bridge infrastructure
771+ let blob_bytes: Vec < u8 > = blob. to_vec ( ) ;
772+ let mut isolate = create_isolate_from_snapshot ( blob_bytes, None ) ;
773+ let scope = & mut v8:: HandleScope :: new ( & mut isolate) ;
774+ let context = v8:: Context :: new ( scope, Default :: default ( ) ) ;
775+ let scope = & mut v8:: ContextScope :: new ( scope, context) ;
776+
777+ // Check that bridge infrastructure from the IIFE is in the default context
778+ let check_code = r#"
779+ (function() {
780+ var results = [];
781+ results.push('_fs=' + (typeof _fs === 'object'));
782+ results.push('_fs.readFile=' + (typeof _fs.readFile === 'function'));
783+ results.push('myLog=' + (typeof myLog === 'function'));
784+ results.push('require=' + (typeof require === 'function'));
785+ results.push('console.log=' + (typeof console.log === 'function'));
786+ results.push('console.error=' + (typeof console.error === 'function'));
787+ results.push('__initialCwd=' + __initialCwd);
788+ results.push('__part16_setup=' + __part16_setup);
789+ return results.join(';');
790+ })()
791+ "# ;
792+ let source = v8:: String :: new ( scope, check_code) . unwrap ( ) ;
793+ let script = v8:: Script :: compile ( scope, source, None ) . unwrap ( ) ;
794+ let result = script. run ( scope) . unwrap ( ) ;
795+ let result_str = result. to_rust_string_lossy ( scope) ;
796+
797+ assert_eq ! (
798+ result_str,
799+ "_fs=true;_fs.readFile=true;myLog=true;require=true;console.log=true;console.error=true;__initialCwd=/;__part16_setup=true" ,
800+ "restored context should have all bridge infrastructure from the IIFE"
801+ ) ;
802+ }
803+
804+ // --- Part 17: SnapshotCache works with context-snapshot create_snapshot ---
805+ // Verifies cache hit/miss still works now that create_snapshot registers stubs.
806+ {
807+ let cache = SnapshotCache :: new ( 4 ) ;
808+ let code = r#"
809+ (function() {
810+ // Verify stubs are present (create_snapshot registers them)
811+ if (typeof _log !== 'function') throw new Error('no _log stub');
812+ if (typeof _processConfig !== 'object') throw new Error('no _processConfig');
813+ globalThis.__cached_context = true;
814+ })();
815+ "# ;
816+
817+ let arc1 = cache. get_or_create ( code) . expect ( "first get_or_create" ) ;
818+ let arc2 = cache. get_or_create ( code) . expect ( "second get_or_create" ) ;
819+ assert ! ( Arc :: ptr_eq( & arc1, & arc2) , "cache hit should return same Arc" ) ;
820+
821+ // Verify blob is usable
822+ let mut isolate = create_isolate_from_snapshot ( ( * arc1) . clone ( ) , None ) ;
823+ assert_eq ! ( eval( & mut isolate, "1 + 1" ) , "2" ) ;
824+ }
608825 }
609826}
0 commit comments