@@ -24,6 +24,19 @@ pub enum Token {
2424 Text ( String ) ,
2525}
2626
27+ impl Token {
28+ fn is_path_derived ( & self ) -> bool {
29+ matches ! (
30+ self ,
31+ Token :: Placeholder
32+ | Token :: Basename
33+ | Token :: Parent
34+ | Token :: NoExt
35+ | Token :: BasenameNoExt
36+ )
37+ }
38+ }
39+
2740impl Display for Token {
2841 fn fmt ( & self , f : & mut Formatter ) -> fmt:: Result {
2942 match * self {
@@ -110,8 +123,26 @@ impl FormatTemplate {
110123 /// the path separator in all placeholder tokens. Fixed text and tokens are not affected by
111124 /// path separator substitution.
112125 pub fn generate ( & self , path : impl AsRef < Path > , path_separator : Option < & str > ) -> OsString {
126+ self . generate_impl ( path. as_ref ( ) , path_separator, false )
127+ }
128+
129+ /// Like `generate`, but prepends `./` when a path-derived token produces a leading `-`,
130+ /// so the target of `--exec` does not see it as an option.
131+ pub fn generate_for_exec (
132+ & self ,
133+ path : impl AsRef < Path > ,
134+ path_separator : Option < & str > ,
135+ ) -> OsString {
136+ self . generate_impl ( path. as_ref ( ) , path_separator, true )
137+ }
138+
139+ fn generate_impl (
140+ & self ,
141+ path : & Path ,
142+ path_separator : Option < & str > ,
143+ protect_leading_dash : bool ,
144+ ) -> OsString {
113145 use Token :: * ;
114- let path = path. as_ref ( ) ;
115146
116147 match * self {
117148 Self :: Tokens ( ref tokens) => {
@@ -134,6 +165,16 @@ impl FormatTemplate {
134165 Text ( string) => s. push ( string) ,
135166 }
136167 }
168+
169+ if protect_leading_dash
170+ && tokens. first ( ) . is_some_and ( |t| t. is_path_derived ( ) )
171+ && s. as_encoded_bytes ( ) . first ( ) == Some ( & b'-' )
172+ {
173+ let mut prefixed = OsString :: from ( "./" ) ;
174+ prefixed. push ( s) ;
175+ return prefixed;
176+ }
177+
137178 s
138179 }
139180 Self :: Text ( ref text) => OsString :: from ( text) ,
@@ -278,4 +319,86 @@ mod fmt_tests {
278319 basenameNoExt=file }"
279320 ) ;
280321 }
322+
323+ fn tmpl ( s : & str ) -> FormatTemplate {
324+ FormatTemplate :: parse ( s)
325+ }
326+
327+ #[ test]
328+ fn exec_basename_with_leading_dash_is_guarded ( ) {
329+ let out = tmpl ( "{/}" )
330+ . generate_for_exec ( Path :: new ( "./some/dir/-rf" ) , None )
331+ . into_string ( )
332+ . unwrap ( ) ;
333+ assert_eq ! ( out, "./-rf" ) ;
334+ }
335+
336+ #[ test]
337+ fn exec_basename_no_ext_with_leading_dash_is_guarded ( ) {
338+ let out = tmpl ( "{/.}" )
339+ . generate_for_exec ( Path :: new ( "./some/dir/-evil.txt" ) , None )
340+ . into_string ( )
341+ . unwrap ( ) ;
342+ assert_eq ! ( out, "./-evil" ) ;
343+ }
344+
345+ #[ test]
346+ fn exec_parent_with_leading_dash_is_guarded ( ) {
347+ let out = tmpl ( "{//}" )
348+ . generate_for_exec ( Path :: new ( "-startdir/inner/file.txt" ) , None )
349+ . into_string ( )
350+ . unwrap ( ) ;
351+ assert_eq ! ( out, "./-startdir/inner" ) ;
352+ }
353+
354+ #[ test]
355+ fn exec_plain_placeholder_already_safe ( ) {
356+ let out = tmpl ( "{}" )
357+ . generate_for_exec ( Path :: new ( "./some/dir/-rf" ) , None )
358+ . into_string ( )
359+ . unwrap ( ) ;
360+ assert_eq ! ( out, "./some/dir/-rf" ) ;
361+ }
362+
363+ #[ test]
364+ fn exec_user_literal_dash_prefix_not_rewritten ( ) {
365+ let out = tmpl ( "-{/}" )
366+ . generate_for_exec ( Path :: new ( "./some/dir/normal.txt" ) , None )
367+ . into_string ( )
368+ . unwrap ( ) ;
369+ assert_eq ! ( out, "-normal.txt" ) ;
370+ }
371+
372+ #[ test]
373+ fn exec_user_literal_dash_before_dash_basename ( ) {
374+ let out = tmpl ( "-{/}" )
375+ . generate_for_exec ( Path :: new ( "./some/dir/-name" ) , None )
376+ . into_string ( )
377+ . unwrap ( ) ;
378+ assert_eq ! ( out, "--name" ) ;
379+ }
380+
381+ #[ test]
382+ fn exec_suffix_after_placeholder_preserves_guard ( ) {
383+ let out = tmpl ( "{/}.bak" )
384+ . generate_for_exec ( Path :: new ( "./some/dir/-foo" ) , None )
385+ . into_string ( )
386+ . unwrap ( ) ;
387+ assert_eq ! ( out, "./-foo.bak" ) ;
388+ }
389+
390+ #[ test]
391+ fn format_mode_does_not_protect_leading_dash ( ) {
392+ let out = tmpl ( "{/}" )
393+ . generate ( Path :: new ( "./some/dir/-rf" ) , None )
394+ . into_string ( )
395+ . unwrap ( ) ;
396+ assert_eq ! ( out, "-rf" ) ;
397+ }
398+
399+ #[ test]
400+ fn exec_empty_result_not_prefixed ( ) {
401+ let out = tmpl ( "{/}" ) . generate_for_exec ( Path :: new ( "" ) , None ) ;
402+ assert ! ( out. is_empty( ) ) ;
403+ }
281404}
0 commit comments