11use std:: future:: Future ;
22use std:: pin:: Pin ;
33
4+ use futures:: future:: join_all;
5+
46use std:: {
57 collections:: HashSet ,
68 ffi:: OsStr ,
@@ -24,7 +26,7 @@ use wasmtime::{
2426 component:: { Component , Linker , ResourceTable } ,
2527 Config , Engine , Store ,
2628} ;
27- use wasmtime_wasi:: { p2, WasiCtx , WasiCtxBuilder , WasiCtxView , WasiView } ;
29+ use wasmtime_wasi:: { p2, DirPerms , FilePerms , WasiCtx , WasiCtxBuilder , WasiCtxView , WasiView } ;
2830use wasmtime_wasi_http:: { WasiHttpCtx , WasiHttpView } ;
2931
3032mod bindings {
@@ -74,7 +76,7 @@ pub async fn load_wasm_class_extenders(
7476 plugin_path : & Path ,
7577 plugin_cache_path : & Path ,
7678 db : & Db ,
77- ) -> Vec < ClassExtender > {
79+ ) -> AtomicResult < Vec < ClassExtender > > {
7880 // Create the plugin directory if it doesn't exist
7981 let plugin_dir = plugin_path. join ( CLASS_EXTENDER_DIR_NAME ) ;
8082
@@ -91,7 +93,7 @@ pub async fn load_wasm_class_extenders(
9193 "Created empty Wasm extender directory (drop .wasm files here to enable runtime plugins)"
9294 ) ;
9395 }
94- return Vec :: new ( ) ;
96+ return Ok ( Vec :: new ( ) ) ;
9597 }
9698
9799 if !plugin_cache_path. exists ( ) {
@@ -108,7 +110,7 @@ pub async fn load_wasm_class_extenders(
108110 Ok ( engine) => Arc :: new ( engine) ,
109111 Err ( err) => {
110112 error ! ( error = %err, "Failed to initialize Wasm engine. Skipping dynamic class extenders" ) ;
111- return Vec :: new ( ) ;
113+ return Ok ( Vec :: new ( ) ) ;
112114 }
113115 } ;
114116
@@ -119,43 +121,72 @@ pub async fn load_wasm_class_extenders(
119121
120122 let wasm_files = find_wasm_files ( & plugin_dir) ;
121123
122- for path in wasm_files {
123- let wasm_bytes = match std:: fs:: read ( & path) {
124- Ok ( bytes) => bytes,
125- Err ( e) => {
126- error ! ( "Failed to read Wasm file at {}: {}" , path. display( ) , e) ;
127- continue ;
128- }
129- } ;
124+ let futures = wasm_files. into_iter ( ) . map ( |path| {
125+ let plugin_dir = plugin_dir. clone ( ) ;
126+ let plugin_cache_path = plugin_cache_path. to_path_buf ( ) ;
127+ let engine = engine. clone ( ) ;
128+ let db = db. clone ( ) ;
130129
131- let hash = digest ( & SHA256 , & wasm_bytes) ;
132- let hash_hex = hex_encode ( hash. as_ref ( ) ) ;
133- let cwasm_filename = format ! ( "{}.cwasm" , hash_hex) ;
134- let cwasm_path = plugin_cache_path. join ( cwasm_filename) ;
130+ async move {
131+ let owned_folder_path = setup_plugin_data_dir ( & path, & plugin_dir) ;
135132
136- used_cwasm_files. insert ( cwasm_path. clone ( ) ) ;
137-
138- match WasmPlugin :: load ( engine. clone ( ) , & wasm_bytes, & path, & cwasm_path, db) . await {
139- Ok ( plugin) => {
140- info ! (
141- "Loaded {}" ,
142- path. file_name( ) . unwrap_or( OsStr :: new( "Unknown" ) ) . display( )
143- ) ;
144- extenders. push ( plugin. into_class_extender ( ) ) ;
145- }
146- Err ( err) => {
147- error ! (
148- error = %err,
149- path = %path. display( ) ,
150- "Failed to load Wasm class extender"
151- ) ;
133+ let wasm_bytes = match std:: fs:: read ( & path) {
134+ Ok ( bytes) => bytes,
135+ Err ( e) => {
136+ error ! ( "Failed to read Wasm file at {}: {}" , path. display( ) , e) ;
137+ return None ;
138+ }
139+ } ;
140+
141+ let hash = digest ( & SHA256 , & wasm_bytes) ;
142+ let hash_hex = hex_encode ( hash. as_ref ( ) ) ;
143+ let cwasm_filename = format ! ( "{}.cwasm" , hash_hex) ;
144+ let cwasm_path = plugin_cache_path. join ( cwasm_filename) ;
145+
146+ let cwasm_path_ret = cwasm_path. clone ( ) ;
147+
148+ match WasmPlugin :: load (
149+ engine. clone ( ) ,
150+ & wasm_bytes,
151+ & path,
152+ & cwasm_path,
153+ owned_folder_path,
154+ & db,
155+ )
156+ . await
157+ {
158+ Ok ( plugin) => {
159+ info ! (
160+ "Loaded {}" ,
161+ path. file_name( ) . unwrap_or( OsStr :: new( "Unknown" ) ) . display( )
162+ ) ;
163+ Some ( ( Some ( plugin. into_class_extender ( ) ) , cwasm_path_ret) )
164+ }
165+ Err ( err) => {
166+ error ! (
167+ error = %err,
168+ path = %path. display( ) ,
169+ "Failed to load Wasm class extender"
170+ ) ;
171+ Some ( ( None , cwasm_path_ret) )
172+ }
152173 }
153174 }
175+ } ) ;
176+
177+ let results = join_all ( futures) . await ;
178+
179+ for res in results. into_iter ( ) . flatten ( ) {
180+ let ( extender_opt, cwasm_path) = res;
181+ used_cwasm_files. insert ( cwasm_path) ;
182+ if let Some ( extender) = extender_opt {
183+ extenders. push ( extender) ;
184+ }
154185 }
155186
156187 cleanup_cache ( & plugin_cache_path, & used_cwasm_files) ;
157188
158- extenders
189+ Ok ( extenders)
159190}
160191
161192fn build_engine ( ) -> AtomicResult < Engine > {
@@ -174,6 +205,7 @@ struct WasmPluginInner {
174205 engine : Arc < Engine > ,
175206 component : Component ,
176207 path : PathBuf ,
208+ owned_folder_path : Option < PathBuf > ,
177209 class_url : String ,
178210 db : Arc < Db > ,
179211}
@@ -184,6 +216,7 @@ impl WasmPlugin {
184216 wasm_bytes : & [ u8 ] ,
185217 path : & Path ,
186218 cwasm_path : & Path ,
219+ owned_folder_path : Option < PathBuf > ,
187220 db : & Db ,
188221 ) -> AtomicResult < Self > {
189222 let db = Arc :: new ( db. clone ( ) ) ;
@@ -218,6 +251,7 @@ impl WasmPlugin {
218251 engine : engine. clone ( ) ,
219252 component,
220253 path : path. to_path_buf ( ) ,
254+ owned_folder_path,
221255 class_url : String :: new ( ) ,
222256 db : Arc :: clone ( & db) ,
223257 } ) ,
@@ -229,6 +263,7 @@ impl WasmPlugin {
229263 engine,
230264 component : runtime. inner . component . clone ( ) ,
231265 path : runtime. inner . path . clone ( ) ,
266+ owned_folder_path : runtime. inner . owned_folder_path . clone ( ) ,
232267 class_url,
233268 db,
234269 } ) ,
@@ -312,7 +347,7 @@ impl WasmPlugin {
312347 async fn instantiate ( & self ) -> AtomicResult < ( bindings:: ClassExtender , Store < PluginHostState > ) > {
313348 let mut store = Store :: new (
314349 & self . inner . engine ,
315- PluginHostState :: new ( Arc :: clone ( & self . inner . db ) ) ?,
350+ PluginHostState :: new ( Arc :: clone ( & self . inner . db ) , & self . inner . owned_folder_path ) ?,
316351 ) ;
317352 let mut linker = Linker :: new ( & self . inner . engine ) ;
318353 p2:: add_to_linker_async ( & mut linker) . map_err ( |err| AtomicError :: from ( err. to_string ( ) ) ) ?;
@@ -403,13 +438,25 @@ struct PluginHostState {
403438}
404439
405440impl PluginHostState {
406- fn new ( db : Arc < Db > ) -> AtomicResult < Self > {
441+ fn new ( db : Arc < Db > , owned_folder_path : & Option < PathBuf > ) -> AtomicResult < Self > {
407442 let mut builder = WasiCtxBuilder :: new ( ) ;
408443 builder
409444 . inherit_stdout ( )
410445 . inherit_stderr ( )
411446 . inherit_stdin ( )
412447 . inherit_network ( ) ;
448+
449+ if let Some ( owned_folder_path) = owned_folder_path {
450+ builder
451+ . preopened_dir (
452+ owned_folder_path. clone ( ) ,
453+ "/" ,
454+ DirPerms :: READ | DirPerms :: MUTATE ,
455+ FilePerms :: WRITE | FilePerms :: READ ,
456+ )
457+ . map_err ( |e| AtomicError :: from ( format ! ( "Failed to preopen directory: {}" , e) ) ) ?;
458+ }
459+
413460 let ctx = builder. build ( ) ;
414461 Ok ( Self {
415462 table : ResourceTable :: new ( ) ,
@@ -495,23 +542,59 @@ fn find_wasm_files(dir: &Path) -> Vec<PathBuf> {
495542 if let Ok ( entries) = std:: fs:: read_dir ( dir) {
496543 for entry in entries. flatten ( ) {
497544 let path = entry. path ( ) ;
498- if path. is_dir ( ) {
499- if let Ok ( sub_entries) = std:: fs:: read_dir ( & path) {
500- for sub_entry in sub_entries. flatten ( ) {
501- let sub_path = sub_entry. path ( ) ;
502- if sub_path. extension ( ) == Some ( OsStr :: new ( "wasm" ) ) {
503- files. push ( sub_path) ;
504- }
505- }
506- }
507- } else if path. extension ( ) == Some ( OsStr :: new ( "wasm" ) ) {
545+ if path. is_file ( ) && path. extension ( ) == Some ( OsStr :: new ( "wasm" ) ) {
508546 files. push ( path) ;
509547 }
510548 }
511549 }
512550 files
513551}
514552
553+ fn setup_plugin_data_dir ( wasm_file_path : & Path , plugin_dir : & Path ) -> Option < PathBuf > {
554+ let filename = wasm_file_path. file_name ( ) . and_then ( |s| s. to_str ( ) ) ?;
555+
556+ // Remove .wasm extension
557+ let stem = wasm_file_path
558+ . file_stem ( )
559+ . and_then ( |s| s. to_str ( ) )
560+ . unwrap_or ( filename) ;
561+
562+ let stem_path = Path :: new ( stem) ;
563+
564+ // If there is no second extension (e.g. just my-plugin.wasm), we don't grant access to a folder.
565+ // This is to prevent plugins from accessing arbitrary folders.
566+ // Only namespaced plugins (e.g. google.calendar.wasm or my-plugin.plugin.wasm) get a folder.
567+ if stem_path. extension ( ) . is_none ( ) {
568+ return None ;
569+ }
570+
571+ // Remove the second extension (e.g. .plugin in my_script.plugin.wasm), if present.
572+ // This allows for any suffix without dots.
573+ let plugin_name = stem_path
574+ . file_stem ( )
575+ . and_then ( |s| s. to_str ( ) )
576+ . unwrap_or ( stem) ;
577+
578+ let data_dir = plugin_dir. join ( plugin_name) ;
579+
580+ if !data_dir. exists ( ) {
581+ if let Err ( err) = std:: fs:: create_dir_all ( & data_dir) {
582+ warn ! (
583+ error = %err,
584+ path = %data_dir. display( ) ,
585+ "Failed to create data directory for plugin"
586+ ) ;
587+ return None ;
588+ }
589+ }
590+
591+ if data_dir. exists ( ) {
592+ Some ( data_dir)
593+ } else {
594+ None
595+ }
596+ }
597+
515598fn compile_and_save_component (
516599 engine : & Engine ,
517600 wasm_bytes : & [ u8 ] ,
0 commit comments