@@ -5,7 +5,6 @@ use rustc_version::version;
55use semver:: Version ;
66use sha2:: { Digest , Sha256 } ;
77use std:: {
8- borrow:: Cow ,
98 collections:: HashSet ,
109 env,
1110 ffi:: OsStr ,
@@ -283,21 +282,7 @@ impl Cmd {
283282 // optimization using markers.
284283 cmd. env ( "SOROBAN_SDK_BUILD_SYSTEM_SUPPORTS_SPEC_SHAKING_V2" , "1" ) ;
285284
286- let mut cmd_str_parts = Vec :: < String > :: new ( ) ;
287- cmd_str_parts. extend ( cmd. get_envs ( ) . map ( |( key, val) | {
288- format ! (
289- "{}={}" ,
290- key. to_string_lossy( ) ,
291- shell_escape:: escape( val. unwrap_or_default( ) . to_string_lossy( ) )
292- )
293- } ) ) ;
294- cmd_str_parts. push ( "cargo" . to_string ( ) ) ;
295- cmd_str_parts. extend (
296- cmd. get_args ( )
297- . map ( OsStr :: to_string_lossy)
298- . map ( Cow :: into_owned) ,
299- ) ;
300- let cmd_str = cmd_str_parts. join ( " " ) ;
285+ let cmd_str = serialize_command ( & cmd) ;
301286
302287 if self . print_commands_only {
303288 println ! ( "{cmd_str}" ) ;
@@ -589,6 +574,24 @@ impl Cmd {
589574 }
590575}
591576
577+ fn serialize_command ( cmd : & Command ) -> String {
578+ let mut parts = Vec :: < String > :: new ( ) ;
579+ parts. extend ( cmd. get_envs ( ) . map ( |( key, val) | {
580+ format ! (
581+ "{}={}" ,
582+ key. to_string_lossy( ) ,
583+ shell_escape:: escape( val. unwrap_or_default( ) . to_string_lossy( ) )
584+ )
585+ } ) ) ;
586+ parts. push ( cmd. get_program ( ) . to_string_lossy ( ) . into_owned ( ) ) ;
587+ parts. extend (
588+ cmd. get_args ( )
589+ . map ( OsStr :: to_string_lossy)
590+ . map ( |a| shell_escape:: escape ( a) . into_owned ( ) ) ,
591+ ) ;
592+ parts. join ( " " )
593+ }
594+
592595/// Configure cargo/rustc to replace absolute paths in panic messages / debuginfo
593596/// with relative paths.
594597///
@@ -812,3 +815,34 @@ pub fn filter_and_dedup_spec(
812815 }
813816 Ok ( filtered_xdr)
814817}
818+
819+ #[ cfg( test) ]
820+ mod tests {
821+ use super :: * ;
822+
823+ #[ test]
824+ fn serialize_command_shell_escapes_args_with_metacharacters ( ) {
825+ let raw_arg = "--manifest-path=/path/to/contract;touch PWNED;#/Cargo.toml" ;
826+ let escaped_arg = shell_escape:: escape ( raw_arg. into ( ) ) . into_owned ( ) ;
827+
828+ let mut cmd = Command :: new ( "cargo" ) ;
829+ cmd. arg ( "rustc" ) ;
830+ cmd. arg ( raw_arg) ;
831+
832+ let output = serialize_command ( & cmd) ;
833+
834+ // The full escaped form of the argument must appear verbatim.
835+ assert ! (
836+ output. contains( & escaped_arg) ,
837+ "expected escaped arg {escaped_arg:?} in output: {output}"
838+ ) ;
839+
840+ // Round-trip through shlex: the metacharacter-laden arg must parse back
841+ // as a single token equal to the original.
842+ let tokens = shlex:: split ( & output) . expect ( "serialize_command output must be valid shell" ) ;
843+ assert ! (
844+ tokens. iter( ) . any( |t| t == raw_arg) ,
845+ "shlex round-trip failed: {raw_arg:?} not found as a single token in {tokens:?}"
846+ ) ;
847+ }
848+ }
0 commit comments