11use std:: {
2- path:: { Path , PathBuf } ,
2+ path:: { Component , Path , PathBuf } ,
33 sync:: Arc ,
44} ;
55
@@ -14,51 +14,117 @@ use crate::{
1414 model:: change_list:: ClDiffFile ,
1515} ;
1616
17- fn normalize_change_path_for_repo ( repo_path : & str , path : & Path ) -> ProjectRelativePath {
17+ fn is_safe_normalized_path ( path : & str ) -> bool {
18+ path. is_empty ( )
19+ || ( !path. contains ( "//" )
20+ && Path :: new ( path)
21+ . components ( )
22+ . all ( |component| matches ! ( component, Component :: Normal ( _) ) ) )
23+ }
24+
25+ fn normalize_change_path_for_repo_with_prefix (
26+ repo_prefix : & str ,
27+ repo_prefix_with_slash : Option < & str > ,
28+ path : & Path ,
29+ ) -> Option < ProjectRelativePath > {
1830 let raw = path
1931 . to_string_lossy ( )
2032 . replace ( '\\' , "/" )
2133 . trim_start_matches ( '/' )
2234 . to_string ( ) ;
23- let repo_prefix = repo_path. trim_matches ( '/' ) ;
2435
2536 let normalized = if repo_prefix. is_empty ( ) {
2637 raw
2738 } else if raw == repo_prefix {
2839 String :: new ( )
29- } else if let Some ( stripped) = raw. strip_prefix ( & format ! ( "{repo_prefix}/" ) ) {
30- stripped. to_string ( )
40+ } else if let Some ( prefix) = repo_prefix_with_slash {
41+ if let Some ( stripped) = raw. strip_prefix ( prefix) {
42+ stripped. to_string ( )
43+ } else {
44+ raw
45+ }
3146 } else {
3247 raw
3348 } ;
3449
35- ProjectRelativePath :: new ( & normalized)
50+ if !is_safe_normalized_path ( & normalized) {
51+ tracing:: warn!(
52+ path = %normalized,
53+ "Dropping unsafe build change path after normalization."
54+ ) ;
55+ return None ;
56+ }
57+
58+ Some ( ProjectRelativePath :: new ( & normalized) )
59+ }
60+
61+ #[ cfg( test) ]
62+ fn normalize_change_path_for_repo ( repo_path : & str , path : & Path ) -> Option < ProjectRelativePath > {
63+ let repo_prefix = repo_path. trim_matches ( '/' ) ;
64+ let repo_prefix_with_slash = ( !repo_prefix. is_empty ( ) ) . then ( || format ! ( "{repo_prefix}/" ) ) ;
65+ normalize_change_path_for_repo_with_prefix ( repo_prefix, repo_prefix_with_slash. as_deref ( ) , path)
66+ }
67+
68+ fn push_change_if_valid (
69+ changes : & mut Vec < Status < ProjectRelativePath > > ,
70+ status_builder : impl FnOnce ( ProjectRelativePath ) -> Status < ProjectRelativePath > ,
71+ normalized : Option < ProjectRelativePath > ,
72+ ) {
73+ if let Some ( path) = normalized {
74+ changes. push ( status_builder ( path) ) ;
75+ }
3676}
3777
3878fn build_changes_for_repo (
3979 repo_path : & str ,
4080 cl_diff_files : Vec < ClDiffFile > ,
4181) -> Result < Vec < Status < ProjectRelativePath > > , MegaError > {
42- let to_project_relative = |path : & Path | -> Result < ProjectRelativePath , MegaError > {
43- Ok ( normalize_change_path_for_repo ( repo_path, path) )
82+ let repo_prefix = repo_path. trim_matches ( '/' ) ;
83+ let repo_prefix_with_slash = ( !repo_prefix. is_empty ( ) ) . then ( || format ! ( "{repo_prefix}/" ) ) ;
84+ let to_project_relative = |path : & Path | {
85+ normalize_change_path_for_repo_with_prefix (
86+ repo_prefix,
87+ repo_prefix_with_slash. as_deref ( ) ,
88+ path,
89+ )
4490 } ;
4591
4692 let mut counter_changes = Vec :: new ( ) ;
4793 for change in cl_diff_files {
4894 match change {
4995 ClDiffFile :: New ( path, _) => {
50- counter_changes. push ( Status :: Added ( to_project_relative ( & path) ?) ) ;
96+ push_change_if_valid (
97+ & mut counter_changes,
98+ Status :: Added ,
99+ to_project_relative ( & path) ,
100+ ) ;
51101 }
52102 ClDiffFile :: Deleted ( path, _) => {
53- counter_changes. push ( Status :: Removed ( to_project_relative ( & path) ?) ) ;
103+ push_change_if_valid (
104+ & mut counter_changes,
105+ Status :: Removed ,
106+ to_project_relative ( & path) ,
107+ ) ;
54108 }
55109 ClDiffFile :: Modified ( path, _, _) => {
56- counter_changes. push ( Status :: Modified ( to_project_relative ( & path) ?) ) ;
110+ push_change_if_valid (
111+ & mut counter_changes,
112+ Status :: Modified ,
113+ to_project_relative ( & path) ,
114+ ) ;
57115 }
58116 ClDiffFile :: Renamed ( old_path, new_path, _, _, _)
59117 | ClDiffFile :: Moved ( old_path, new_path, _, _, _) => {
60- counter_changes. push ( Status :: Removed ( to_project_relative ( & old_path) ?) ) ;
61- counter_changes. push ( Status :: Added ( to_project_relative ( & new_path) ?) ) ;
118+ push_change_if_valid (
119+ & mut counter_changes,
120+ Status :: Removed ,
121+ to_project_relative ( & old_path) ,
122+ ) ;
123+ push_change_if_valid (
124+ & mut counter_changes,
125+ Status :: Added ,
126+ to_project_relative ( & new_path) ,
127+ ) ;
62128 }
63129 }
64130 }
@@ -128,22 +194,44 @@ mod tests {
128194 fn test_normalize_change_path_for_repo_strips_repo_prefix_for_local_files ( ) {
129195 assert_eq ! (
130196 normalize_change_path_for_repo( "/project/buck2_test" , & PathBuf :: from( "src/main.rs" ) ) ,
131- ProjectRelativePath :: new( "src/main.rs" )
197+ Some ( ProjectRelativePath :: new( "src/main.rs" ) )
132198 ) ;
133199 assert_eq ! (
134200 normalize_change_path_for_repo(
135201 "/project/buck2_test" ,
136202 & PathBuf :: from( "project/buck2_test/src/generated.rs" )
137203 ) ,
138- ProjectRelativePath :: new( "src/generated.rs" )
204+ Some ( ProjectRelativePath :: new( "src/generated.rs" ) )
139205 ) ;
140206 }
141207
142208 #[ test]
143209 fn test_normalize_change_path_for_repo_keeps_external_shared_paths ( ) {
144210 assert_eq ! (
145211 normalize_change_path_for_repo( "/project/buck2_test" , & PathBuf :: from( "common/lib.rs" ) ) ,
146- ProjectRelativePath :: new( "common/lib.rs" )
212+ Some ( ProjectRelativePath :: new( "common/lib.rs" ) )
213+ ) ;
214+ }
215+
216+ #[ test]
217+ fn test_normalize_change_path_for_repo_rejects_unsafe_paths ( ) {
218+ assert_eq ! (
219+ normalize_change_path_for_repo( "/project/buck2_test" , & PathBuf :: from( "../secret.rs" ) ) ,
220+ None
221+ ) ;
222+ assert_eq ! (
223+ normalize_change_path_for_repo(
224+ "/project/buck2_test" ,
225+ & PathBuf :: from( "project/buck2_test/../../secret.rs" )
226+ ) ,
227+ None
228+ ) ;
229+ assert_eq ! (
230+ normalize_change_path_for_repo(
231+ "/project/buck2_test" ,
232+ & PathBuf :: from( "project//buck2_test/src/main.rs" )
233+ ) ,
234+ None
147235 ) ;
148236 }
149237
@@ -184,4 +272,28 @@ mod tests {
184272 ]
185273 ) ;
186274 }
275+
276+ #[ test]
277+ fn test_build_changes_filters_unsafe_paths ( ) {
278+ let changes = build_changes_for_repo (
279+ "/project/buck2_test" ,
280+ vec ! [
281+ ClDiffFile :: Modified (
282+ PathBuf :: from( "src/main.rs" ) ,
283+ ObjectHash :: from_str( "1111111111111111111111111111111111111111" ) . unwrap( ) ,
284+ ObjectHash :: from_str( "2222222222222222222222222222222222222222" ) . unwrap( ) ,
285+ ) ,
286+ ClDiffFile :: New (
287+ PathBuf :: from( "../outside.rs" ) ,
288+ ObjectHash :: from_str( "3333333333333333333333333333333333333333" ) . unwrap( ) ,
289+ ) ,
290+ ] ,
291+ )
292+ . unwrap ( ) ;
293+
294+ assert_eq ! (
295+ changes,
296+ vec![ Status :: Modified ( ProjectRelativePath :: new( "src/main.rs" ) ) ]
297+ ) ;
298+ }
187299}
0 commit comments