@@ -13,32 +13,36 @@ const RESERVED_MOUNT_TARGETS: &[&str] = &[
1313] ;
1414
1515/// Validate a non-empty driver mount source.
16- pub fn validate_mount_source ( source : & str , field : & str ) -> Result < String , String > {
17- let source = source. trim ( ) ;
16+ pub fn validate_mount_source ( source : & str , field : & str ) -> Result < ( ) , String > {
1817 if source. is_empty ( ) {
1918 return Err ( format ! ( "{field} must not be empty" ) ) ;
2019 }
20+ if source != source. trim ( ) {
21+ return Err ( format ! ( "{field} must not contain surrounding whitespace" ) ) ;
22+ }
2123 if source. as_bytes ( ) . contains ( & 0 ) {
2224 return Err ( format ! ( "{field} must not contain NUL bytes" ) ) ;
2325 }
24- Ok ( source . to_string ( ) )
26+ Ok ( ( ) )
2527}
2628
2729/// Validate a bind mount source as an absolute host path.
28- pub fn validate_absolute_mount_source ( source : & str , field : & str ) -> Result < String , String > {
29- let source = validate_mount_source ( source, field) ?;
30- if !Path :: new ( & source) . is_absolute ( ) {
30+ pub fn validate_absolute_mount_source ( source : & str , field : & str ) -> Result < ( ) , String > {
31+ validate_mount_source ( source, field) ?;
32+ if !Path :: new ( source) . is_absolute ( ) {
3133 return Err ( format ! ( "{field} must be an absolute host path" ) ) ;
3234 }
33- Ok ( source )
35+ Ok ( ( ) )
3436}
3537
3638/// Validate a relative subpath inside a runtime-managed mount source.
37- pub fn validate_mount_subpath ( subpath : & str ) -> Result < String , String > {
38- let subpath = subpath. trim ( ) ;
39+ pub fn validate_mount_subpath ( subpath : & str ) -> Result < ( ) , String > {
3940 if subpath. is_empty ( ) {
4041 return Err ( "mount subpath must not be empty" . to_string ( ) ) ;
4142 }
43+ if subpath != subpath. trim ( ) {
44+ return Err ( "mount subpath must not contain surrounding whitespace" . to_string ( ) ) ;
45+ }
4246 if subpath. as_bytes ( ) . contains ( & 0 ) {
4347 return Err ( "mount subpath must not contain NUL bytes" . to_string ( ) ) ;
4448 }
@@ -50,57 +54,56 @@ pub fn validate_mount_subpath(subpath: &str) -> Result<String, String> {
5054 {
5155 return Err ( "mount subpath must be relative and must not contain '..'" . to_string ( ) ) ;
5256 }
53- Ok ( subpath . to_string ( ) )
57+ Ok ( ( ) )
5458}
5559
5660/// Validate a container-side mount target for user-supplied driver mounts.
57- pub fn validate_container_mount_target ( target : & str ) -> Result < String , String > {
58- let target = normalize_container_mount_target ( target) ;
61+ pub fn validate_container_mount_target ( target : & str ) -> Result < ( ) , String > {
5962 if target. is_empty ( ) {
6063 return Err ( "mount target must not be empty" . to_string ( ) ) ;
6164 }
65+ if target != target. trim ( ) {
66+ return Err ( "mount target must not contain surrounding whitespace" . to_string ( ) ) ;
67+ }
6268 if target. as_bytes ( ) . contains ( & 0 ) {
6369 return Err ( "mount target must not contain NUL bytes" . to_string ( ) ) ;
6470 }
6571 if !target. starts_with ( '/' ) {
6672 return Err ( "mount target must be an absolute container path" . to_string ( ) ) ;
6773 }
68- if target == "/" {
74+ let path = Path :: new ( target) ;
75+ if path == Path :: new ( "/" ) {
6976 return Err ( "mount target must not be the container root" . to_string ( ) ) ;
7077 }
71- let path = Path :: new ( & target) ;
7278 if path
7379 . components ( )
7480 . any ( |component| matches ! ( component, std:: path:: Component :: ParentDir ) )
7581 {
7682 return Err ( "mount target must not contain '..'" . to_string ( ) ) ;
7783 }
78- if target == "/sandbox" {
84+ if path == Path :: new ( "/sandbox" ) {
7985 return Err ( "mount target '/sandbox' is reserved for the OpenShell workspace" . to_string ( ) ) ;
8086 }
8187 for reserved in RESERVED_MOUNT_TARGETS {
82- if path_is_or_under ( & target , reserved) {
88+ if path_is_or_under ( path , Path :: new ( reserved) ) {
8389 return Err ( format ! (
8490 "mount target '{target}' conflicts with reserved OpenShell path '{reserved}'"
8591 ) ) ;
8692 }
8793 }
88- Ok ( target )
94+ Ok ( ( ) )
8995}
9096
91- fn normalize_container_mount_target ( target : & str ) -> String {
92- let target = target . trim ( ) ;
97+ /// Normalize a validated container-side mount target for semantic comparison.
98+ pub fn normalize_mount_target ( target : & str ) -> String {
9399 if target == "/" {
94100 return target. to_string ( ) ;
95101 }
96102 target. trim_end_matches ( '/' ) . to_string ( )
97103}
98104
99- fn path_is_or_under ( path : & str , parent : & str ) -> bool {
100- path == parent
101- || path
102- . strip_prefix ( parent)
103- . is_some_and ( |rest| rest. starts_with ( '/' ) )
105+ fn path_is_or_under ( path : & Path , parent : & Path ) -> bool {
106+ path == parent || path. starts_with ( parent)
104107}
105108
106109#[ cfg( test) ]
@@ -109,10 +112,8 @@ mod tests {
109112
110113 #[ test]
111114 fn container_target_allows_paths_under_workspace ( ) {
112- assert_eq ! (
113- validate_container_mount_target( "/sandbox/work/" ) . unwrap( ) ,
114- "/sandbox/work"
115- ) ;
115+ validate_container_mount_target ( "/sandbox/work/" ) . unwrap ( ) ;
116+ assert_eq ! ( normalize_mount_target( "/sandbox/work/" ) , "/sandbox/work" ) ;
116117 }
117118
118119 #[ test]
@@ -138,16 +139,30 @@ mod tests {
138139
139140 #[ test]
140141 fn container_target_does_not_prefix_match_unrelated_paths ( ) {
141- assert_eq ! (
142- validate_container_mount_target( "/etc/openshell-tools" ) . unwrap( ) ,
143- "/etc/openshell-tools"
144- ) ;
142+ validate_container_mount_target ( "/etc/openshell-tools" ) . unwrap ( ) ;
145143 }
146144
147145 #[ test]
148146 fn mount_subpath_must_be_relative_without_parent_dirs ( ) {
149- assert_eq ! ( validate_mount_subpath( " project/a " ) . unwrap( ) , "project/a" ) ;
147+ assert ! ( validate_mount_subpath( "project/a" ) . is_ok( ) ) ;
148+ assert ! ( validate_mount_subpath( " project/a " ) . is_err( ) ) ;
150149 assert ! ( validate_mount_subpath( "/project" ) . is_err( ) ) ;
151150 assert ! ( validate_mount_subpath( "../project" ) . is_err( ) ) ;
152151 }
152+
153+ #[ test]
154+ fn mount_values_reject_surrounding_whitespace ( ) {
155+ assert_eq ! (
156+ validate_mount_source( " volume " , "volume source" ) . unwrap_err( ) ,
157+ "volume source must not contain surrounding whitespace"
158+ ) ;
159+ assert_eq ! (
160+ validate_absolute_mount_source( " /host/path" , "bind source" ) . unwrap_err( ) ,
161+ "bind source must not contain surrounding whitespace"
162+ ) ;
163+ assert_eq ! (
164+ validate_container_mount_target( "/sandbox/work " ) . unwrap_err( ) ,
165+ "mount target must not contain surrounding whitespace"
166+ ) ;
167+ }
153168}
0 commit comments