@@ -63,156 +63,3 @@ pub fn get_helper(service: &str) -> Option<Box<dyn Helper>> {
6363 _ => None ,
6464 }
6565}
66-
67- // ---------------------------------------------------------------------------
68- // URL safety helpers
69- // ---------------------------------------------------------------------------
70-
71- /// Percent-encode a value for use as a single URL path segment (e.g., file ID,
72- /// calendar ID, message ID). All non-alphanumeric characters are encoded.
73- pub ( crate ) fn encode_path_segment ( s : & str ) -> String {
74- use percent_encoding:: { utf8_percent_encode, NON_ALPHANUMERIC } ;
75- utf8_percent_encode ( s, NON_ALPHANUMERIC ) . to_string ( )
76- }
77-
78- /// Validate a multi-segment resource name (e.g., `spaces/ABC`, `subscriptions/123`).
79- /// Rejects path traversal and control characters while preserving the intentional
80- /// `/`-delimited structure. Returns the validated name or an error with a clear
81- /// message suitable for LLM callers.
82- pub ( crate ) fn validate_resource_name ( s : & str ) -> Result < & str , GwsError > {
83- if s. is_empty ( ) {
84- return Err ( GwsError :: Validation (
85- "Resource name must not be empty" . to_string ( ) ,
86- ) ) ;
87- }
88- if s. split ( '/' ) . any ( |seg| seg == ".." ) {
89- return Err ( GwsError :: Validation ( format ! (
90- "Resource name must not contain path traversal ('..') segments: {s}"
91- ) ) ) ;
92- }
93- if s. contains ( '\0' ) || s. chars ( ) . any ( |c| c. is_control ( ) ) {
94- return Err ( GwsError :: Validation ( format ! (
95- "Resource name contains invalid characters: {s}"
96- ) ) ) ;
97- }
98- // Reject URL-special characters that could inject query params or fragments
99- if s. contains ( '?' ) || s. contains ( '#' ) {
100- return Err ( GwsError :: Validation ( format ! (
101- "Resource name must not contain '?' or '#': {s}"
102- ) ) ) ;
103- }
104- Ok ( s)
105- }
106-
107- #[ cfg( test) ]
108- mod tests {
109- use super :: * ;
110-
111- // -- encode_path_segment --------------------------------------------------
112-
113- #[ test]
114- fn test_encode_path_segment_plain_id ( ) {
115- assert_eq ! ( encode_path_segment( "abc123" ) , "abc123" ) ;
116- }
117-
118- #[ test]
119- fn test_encode_path_segment_email ( ) {
120- // Calendar IDs are often email addresses
121- let encoded = encode_path_segment ( "user@gmail.com" ) ;
122- assert ! ( !encoded. contains( '@' ) ) ;
123- assert ! ( !encoded. contains( '.' ) ) ;
124- }
125-
126- #[ test]
127- fn test_encode_path_segment_query_injection ( ) {
128- // LLM might include query params in an ID by mistake
129- let encoded = encode_path_segment ( "fileid?fields=name" ) ;
130- assert ! ( !encoded. contains( '?' ) ) ;
131- assert ! ( !encoded. contains( '=' ) ) ;
132- }
133-
134- #[ test]
135- fn test_encode_path_segment_fragment_injection ( ) {
136- let encoded = encode_path_segment ( "fileid#section" ) ;
137- assert ! ( !encoded. contains( '#' ) ) ;
138- }
139-
140- #[ test]
141- fn test_encode_path_segment_path_traversal ( ) {
142- // Encoding makes traversal segments harmless
143- let encoded = encode_path_segment ( "../../etc/passwd" ) ;
144- assert ! ( !encoded. contains( '/' ) ) ;
145- assert ! ( !encoded. contains( ".." ) ) ;
146- }
147-
148- #[ test]
149- fn test_encode_path_segment_unicode ( ) {
150- // LLM might pass unicode characters
151- let encoded = encode_path_segment ( "日本語ID" ) ;
152- assert ! ( !encoded. contains( '日' ) ) ;
153- }
154-
155- #[ test]
156- fn test_encode_path_segment_spaces ( ) {
157- let encoded = encode_path_segment ( "my file id" ) ;
158- assert ! ( !encoded. contains( ' ' ) ) ;
159- }
160-
161- #[ test]
162- fn test_encode_path_segment_already_encoded ( ) {
163- // LLM might double-encode by passing pre-encoded values
164- let encoded = encode_path_segment ( "user%40gmail.com" ) ;
165- // The % itself gets encoded to %25, so %40 becomes %2540
166- // This prevents double-encoding issues at the HTTP layer
167- assert ! ( encoded. contains( "%2540" ) ) ;
168- }
169-
170- // -- validate_resource_name -----------------------------------------------
171-
172- #[ test]
173- fn test_validate_resource_name_valid ( ) {
174- assert ! ( validate_resource_name( "spaces/ABC123" ) . is_ok( ) ) ;
175- assert ! ( validate_resource_name( "subscriptions/my-sub" ) . is_ok( ) ) ;
176- assert ! ( validate_resource_name( "@default" ) . is_ok( ) ) ;
177- assert ! ( validate_resource_name( "projects/p1/topics/t1" ) . is_ok( ) ) ;
178- }
179-
180- #[ test]
181- fn test_validate_resource_name_traversal ( ) {
182- assert ! ( validate_resource_name( "../../etc/passwd" ) . is_err( ) ) ;
183- assert ! ( validate_resource_name( "spaces/../other" ) . is_err( ) ) ;
184- assert ! ( validate_resource_name( ".." ) . is_err( ) ) ;
185- }
186-
187- #[ test]
188- fn test_validate_resource_name_control_chars ( ) {
189- assert ! ( validate_resource_name( "spaces/\0 bad" ) . is_err( ) ) ;
190- assert ! ( validate_resource_name( "spaces/\n bad" ) . is_err( ) ) ;
191- assert ! ( validate_resource_name( "spaces/\r bad" ) . is_err( ) ) ;
192- assert ! ( validate_resource_name( "spaces/\t bad" ) . is_err( ) ) ;
193- }
194-
195- #[ test]
196- fn test_validate_resource_name_empty ( ) {
197- assert ! ( validate_resource_name( "" ) . is_err( ) ) ;
198- }
199-
200- #[ test]
201- fn test_validate_resource_name_query_injection ( ) {
202- // LLMs might append query strings or fragments to resource names
203- assert ! ( validate_resource_name( "spaces/ABC?key=val" ) . is_err( ) ) ;
204- assert ! ( validate_resource_name( "spaces/ABC#fragment" ) . is_err( ) ) ;
205- }
206-
207- #[ test]
208- fn test_validate_resource_name_error_messages_are_clear ( ) {
209- let err = validate_resource_name ( "" ) . unwrap_err ( ) ;
210- assert ! ( err. to_string( ) . contains( "must not be empty" ) ) ;
211-
212- let err = validate_resource_name ( "../bad" ) . unwrap_err ( ) ;
213- assert ! ( err. to_string( ) . contains( "path traversal" ) ) ;
214-
215- let err = validate_resource_name ( "bad\0 id" ) . unwrap_err ( ) ;
216- assert ! ( err. to_string( ) . contains( "invalid characters" ) ) ;
217- }
218- }
0 commit comments