@@ -61,16 +61,17 @@ impl WasmToolOptions {
6161 }
6262
6363 let mut merged_env = Vec :: with_capacity ( env. len ( ) + inherit_env. len ( ) ) ;
64- for spec in env {
65- merged_env. push ( parse_env_pair ( spec) ?) ;
66- }
6764 for key in inherit_env {
6865 if key. is_empty ( ) {
6966 bail ! ( "--tool-wasi-env-inherit key must not be empty" ) ;
7067 }
7168 let value = std:: env:: var ( key)
7269 . with_context ( || format ! ( "inherit environment variable {key}" ) ) ?;
73- merged_env. push ( ( key. clone ( ) , value) ) ;
70+ set_env_pair ( & mut merged_env, key. clone ( ) , value) ;
71+ }
72+ for spec in env {
73+ let ( key, value) = parse_env_pair ( spec) ?;
74+ set_env_pair ( & mut merged_env, key, value) ;
7475 }
7576
7677 Ok ( Self {
@@ -134,7 +135,7 @@ impl WasmTool {
134135 }
135136
136137 pub fn invoke ( & self , args : Value ) -> Result < Value > {
137- let request = serde_json:: json!( { "name" : self . name, "args" : args } ) ;
138+ let request = serde_json:: json!( { "name" : & self . name, "args" : args } ) ;
138139 let stdin = serde_json:: to_vec ( & request) ?;
139140 let stdout = MemoryOutputPipe :: new ( self . options . output_limit ) ;
140141 let stderr = MemoryOutputPipe :: new ( self . options . output_limit ) ;
@@ -214,7 +215,7 @@ impl WasmTool {
214215 Ok ( value) => Ok ( value) ,
215216 Err ( err) if stdout_len >= self . options . output_limit => Err ( err) . with_context ( || {
216217 format ! (
217- "Wasm tool {} stdout reached output limit of {} bytes" ,
218+ "Wasm tool {} stdout may have reached output limit of {} bytes" ,
218219 self . name, self . options. output_limit
219220 )
220221 } ) ,
@@ -228,6 +229,7 @@ fn parse_tool_spec(spec: &str) -> Result<(String, PathBuf)> {
228229 . split_once ( '=' )
229230 . ok_or_else ( || anyhow ! ( "--tool must use NAME=WASM syntax: {spec}" ) ) ?;
230231 let name = name. trim ( ) ;
232+ let path = path. trim ( ) ;
231233 if name. is_empty ( ) {
232234 bail ! ( "--tool name must not be empty: {spec}" ) ;
233235 }
@@ -244,10 +246,15 @@ fn parse_wasi_dir(spec: &str, read_only: bool) -> Result<WasiDir> {
244246 let ( host, guest) = if let Some ( idx) = spec. rfind ( ':' ) {
245247 let ( host, guest) = spec. split_at ( idx) ;
246248 let guest = & guest[ 1 ..] ;
247- if guest. starts_with ( '/' ) || guest == "." || guest. starts_with ( "./" ) {
249+ if is_windows_drive_path ( spec, idx) {
250+ ( spec, "/host" )
251+ } else if guest. starts_with ( '/' ) || guest == "." || guest. starts_with ( "./" ) {
248252 ( host, guest)
249253 } else {
250- ( spec, "/host" )
254+ bail ! (
255+ "invalid WASI preopen guest path {:?}: expected absolute path, '.', or './path'" ,
256+ guest
257+ ) ;
251258 }
252259 } else {
253260 ( spec, "/host" )
@@ -279,6 +286,31 @@ fn parse_env_pair(spec: &str) -> Result<(String, String)> {
279286 Ok ( ( key. to_string ( ) , value. to_string ( ) ) )
280287}
281288
289+ fn set_env_pair ( env : & mut Vec < ( String , String ) > , key : String , value : String ) {
290+ if let Some ( ( _, existing) ) = env
291+ . iter_mut ( )
292+ . find ( |( existing_key, _) | existing_key == & key)
293+ {
294+ * existing = value;
295+ } else {
296+ env. push ( ( key, value) ) ;
297+ }
298+ }
299+
300+ fn is_windows_drive_path ( spec : & str , colon_idx : usize ) -> bool {
301+ colon_idx == 1
302+ && spec
303+ . as_bytes ( )
304+ . first ( )
305+ . map ( |b| b. is_ascii_alphabetic ( ) )
306+ . unwrap_or ( false )
307+ && spec
308+ . as_bytes ( )
309+ . get ( 2 )
310+ . map ( |b| * b == b'/' || * b == b'\\' )
311+ . unwrap_or ( false )
312+ }
313+
282314fn pipe_text ( pipe : & MemoryOutputPipe ) -> String {
283315 String :: from_utf8_lossy ( & pipe. contents ( ) ) . into_owned ( )
284316}
@@ -790,7 +822,7 @@ mod tests {
790822
791823 #[ test]
792824 fn parse_tool_spec_accepts_valid_name_and_path ( ) {
793- let ( name, path) = parse_tool_spec ( "greet= ./handler.wasm" ) . unwrap ( ) ;
825+ let ( name, path) = parse_tool_spec ( " greet = ./handler.wasm " ) . unwrap ( ) ;
794826 assert_eq ! ( name, "greet" ) ;
795827 assert_eq ! ( path, PathBuf :: from( "./handler.wasm" ) ) ;
796828 }
@@ -805,6 +837,7 @@ mod tests {
805837 "fs_read=handler.wasm" ,
806838 "net_socket=handler.wasm" ,
807839 "greet=" ,
840+ "greet= " ,
808841 ] {
809842 assert ! ( parse_tool_spec( spec) . is_err( ) , "{spec} should fail" ) ;
810843 }
@@ -847,6 +880,27 @@ mod tests {
847880 assert_eq ! ( opts. dirs[ 0 ] . guest, "/host" ) ;
848881 }
849882
883+ #[ test]
884+ fn cli_options_use_last_explicit_env_value_for_duplicate_keys ( ) {
885+ let opts = WasmToolOptions :: from_cli (
886+ & [ ] ,
887+ & [ ] ,
888+ & [
889+ "A=first" . to_string ( ) ,
890+ "B=only" . to_string ( ) ,
891+ "A=second" . to_string ( ) ,
892+ ] ,
893+ & [ ] ,
894+ 1 ,
895+ 1 ,
896+ )
897+ . unwrap ( ) ;
898+ assert_eq ! (
899+ opts. env,
900+ vec![ ( "A" . into( ) , "second" . into( ) ) , ( "B" . into( ) , "only" . into( ) ) ]
901+ ) ;
902+ }
903+
850904 #[ test]
851905 fn cli_options_reject_invalid_values ( ) {
852906 let dir = tempdir ( "invalid-options" ) ;
@@ -857,6 +911,15 @@ mod tests {
857911 ) ;
858912 assert ! ( WasmToolOptions :: from_cli( & [ ] , & [ ] , & [ "=value" . to_string( ) ] , & [ ] , 1 , 1 ) . is_err( ) ) ;
859913 assert ! ( WasmToolOptions :: from_cli( & [ ] , & [ ] , & [ ] , & [ "" . to_string( ) ] , 1 , 1 ) . is_err( ) ) ;
914+ assert ! ( WasmToolOptions :: from_cli(
915+ & [ format!( "{}:relative" , dir. display( ) ) ] ,
916+ & [ ] ,
917+ & [ ] ,
918+ & [ ] ,
919+ 1 ,
920+ 1 ,
921+ )
922+ . is_err( ) ) ;
860923 assert ! ( WasmToolOptions :: from_cli(
861924 & [
862925 format!( "{}:/dup" , dir. display( ) ) ,
@@ -1051,7 +1114,7 @@ mod tests {
10511114 ) ;
10521115 let err = tool. invoke ( json ! ( { } ) ) . unwrap_err ( ) ;
10531116 let msg = err_string ( err) ;
1054- assert ! ( msg. contains( "stdout reached output limit of 8 bytes" ) ) ;
1117+ assert ! ( msg. contains( "stdout may have reached output limit of 8 bytes" ) ) ;
10551118 assert ! ( msg. contains( "wrote non-JSON stdout" ) ) ;
10561119 }
10571120
0 commit comments