1717//! }
1818//! ```
1919
20+ use std:: ffi:: OsString ;
2021use std:: future:: Future ;
2122use std:: io:: ErrorKind ;
2223use std:: path:: Path ;
@@ -27,6 +28,7 @@ use anyhow::Result;
2728
2829/// Alias name for the server sub‑binary.
2930const SERVER_ALIAS : & str = "devo-server" ;
31+ const ALIAS_SENTINEL_PREFIX : & str = "--devo-alias=" ;
3032
3133/// Directory (under DEVO_HOME/tmp/arg0) where alias entries are created.
3234const ALIAS_TEMP_ROOT : & str = "arg0" ;
@@ -111,7 +113,15 @@ where
111113/// Returns the alias name if the process was invoked through one of our
112114/// symlinks or batch scripts, or `None` for a normal `devo` invocation.
113115fn argv0_alias ( ) -> Option < & ' static str > {
114- let argv0 = std:: env:: args_os ( ) . next ( ) . unwrap_or_default ( ) ;
116+ argv0_alias_from_args ( std:: env:: args_os ( ) )
117+ }
118+
119+ fn argv0_alias_from_args < I > ( args : I ) -> Option < & ' static str >
120+ where
121+ I : IntoIterator < Item = OsString > ,
122+ {
123+ let mut args = args. into_iter ( ) ;
124+ let argv0 = args. next ( ) . unwrap_or_default ( ) ;
115125 let file_name = Path :: new ( & argv0)
116126 . file_stem ( )
117127 . and_then ( |s| s. to_str ( ) )
@@ -123,19 +133,39 @@ fn argv0_alias() -> Option<&'static str> {
123133 }
124134
125135 // Windows batch scripts pass the intended alias via a sentinel arg.
126- let mut args = std:: env:: args_os ( ) ;
127- args. next ( ) ; // skip argv[0]
128- if let Some ( sentinel) = args. next ( )
129- && let Some ( s) = sentinel. to_str ( )
130- && let Some ( rest) = s. strip_prefix ( "--devo-alias=" )
131- && rest == SERVER_ALIAS
132- {
136+ if args. next ( ) . as_deref ( ) . is_some_and ( is_server_alias_sentinel) {
133137 return Some ( SERVER_ALIAS ) ;
134138 }
135139
136140 None
137141}
138142
143+ fn is_server_alias_sentinel ( arg : & std:: ffi:: OsStr ) -> bool {
144+ arg. to_str ( )
145+ . and_then ( |s| s. strip_prefix ( ALIAS_SENTINEL_PREFIX ) )
146+ . is_some_and ( |rest| rest == SERVER_ALIAS )
147+ }
148+
149+ fn server_dispatch_args ( ) -> Vec < OsString > {
150+ server_dispatch_args_from ( std:: env:: args_os ( ) )
151+ }
152+
153+ fn server_dispatch_args_from < I > ( args : I ) -> Vec < OsString >
154+ where
155+ I : IntoIterator < Item = OsString > ,
156+ {
157+ let mut args = args. into_iter ( ) ;
158+ let mut filtered = vec ! [ args. next( ) . unwrap_or_else( || OsString :: from( SERVER_ALIAS ) ) ] ;
159+
160+ if let Some ( first_arg) = args. next ( )
161+ && !is_server_alias_sentinel ( & first_arg)
162+ {
163+ filtered. push ( first_arg) ;
164+ }
165+ filtered. extend ( args) ;
166+ filtered
167+ }
168+
139169/// Build a multi‑thread Tokio runtime.
140170fn build_runtime ( ) -> Result < tokio:: runtime:: Runtime > {
141171 let mut builder = tokio:: runtime:: Builder :: new_multi_thread ( ) ;
@@ -150,7 +180,7 @@ fn build_runtime() -> Result<tokio::runtime::Runtime> {
150180/// consumed by `argv0_alias()` so the remaining args are the server's own.
151181async fn run_server_dispatch ( ) {
152182 use clap:: Parser ;
153- let args = devo_server:: ServerProcessArgs :: parse ( ) ;
183+ let args = devo_server:: ServerProcessArgs :: parse_from ( server_dispatch_args ( ) ) ;
154184 let home_dir = match devo_util_paths:: find_devo_home ( ) {
155185 Ok ( d) => d,
156186 Err ( err) => {
@@ -208,7 +238,13 @@ where
208238 I : IntoIterator < Item = std:: result:: Result < ( String , String ) , dotenvy:: Error > > ,
209239{
210240 for ( key, value) in iter. into_iter ( ) . flatten ( ) {
211- if !key. to_ascii_uppercase ( ) . starts_with ( ILLEGAL_ENV_VAR_PREFIX ) {
241+ // Avoid allocating an uppercased copy for every dotenv key; the
242+ // reserved prefix is ASCII, so byte-wise case folding is equivalent.
243+ let has_illegal_prefix = key
244+ . as_bytes ( )
245+ . get ( ..ILLEGAL_ENV_VAR_PREFIX . len ( ) )
246+ . is_some_and ( |prefix| prefix. eq_ignore_ascii_case ( ILLEGAL_ENV_VAR_PREFIX . as_bytes ( ) ) ) ;
247+ if !has_illegal_prefix {
212248 // SAFETY: called before any threads are spawned.
213249 unsafe { std:: env:: set_var ( & key, & value) } ;
214250 }
@@ -384,6 +420,7 @@ struct PathGuard {
384420
385421#[ cfg( test) ]
386422mod tests {
423+ use std:: ffi:: OsString ;
387424 use std:: fs;
388425 use std:: io:: ErrorKind ;
389426 use std:: path:: Path ;
@@ -409,6 +446,58 @@ mod tests {
409446 assert_eq ! ( super :: argv0_alias( ) , None ) ;
410447 }
411448
449+ #[ test]
450+ fn argv0_alias_detects_batch_sentinel ( ) {
451+ let args = vec ! [
452+ OsString :: from( "devo.exe" ) ,
453+ OsString :: from( "--devo-alias=devo-server" ) ,
454+ OsString :: from( "--transport" ) ,
455+ OsString :: from( "stdio" ) ,
456+ ] ;
457+
458+ assert_eq ! (
459+ super :: argv0_alias_from_args( args) ,
460+ Some ( super :: SERVER_ALIAS )
461+ ) ;
462+ }
463+
464+ #[ test]
465+ fn server_dispatch_args_remove_batch_alias_sentinel ( ) {
466+ let args = vec ! [
467+ OsString :: from( "devo.exe" ) ,
468+ OsString :: from( "--devo-alias=devo-server" ) ,
469+ OsString :: from( "--transport" ) ,
470+ OsString :: from( "stdio" ) ,
471+ ] ;
472+
473+ assert_eq ! (
474+ super :: server_dispatch_args_from( args) ,
475+ vec![
476+ OsString :: from( "devo.exe" ) ,
477+ OsString :: from( "--transport" ) ,
478+ OsString :: from( "stdio" )
479+ ]
480+ ) ;
481+ }
482+
483+ #[ test]
484+ fn server_dispatch_args_keep_direct_alias_args ( ) {
485+ let args = vec ! [
486+ OsString :: from( "devo-server" ) ,
487+ OsString :: from( "--transport" ) ,
488+ OsString :: from( "stdio" ) ,
489+ ] ;
490+
491+ assert_eq ! (
492+ super :: server_dispatch_args_from( args) ,
493+ vec![
494+ OsString :: from( "devo-server" ) ,
495+ OsString :: from( "--transport" ) ,
496+ OsString :: from( "stdio" )
497+ ]
498+ ) ;
499+ }
500+
412501 #[ test]
413502 fn lock_unavailable_detection_handles_would_block ( ) {
414503 assert_eq ! (
0 commit comments