@@ -107,7 +107,7 @@ fn system_gateway_metadata_path(name: &str) -> PathBuf {
107107
108108fn resolve_gateway_metadata_path ( name : & str ) -> Result < Option < ( PathBuf , GatewayMetadataSource ) > > {
109109 let user = user_gateway_metadata_path ( name) ?;
110- if user . exists ( ) {
110+ if user_entry_shadows_system ( & user ) {
111111 return Ok ( Some ( ( user, GatewayMetadataSource :: User ) ) ) ;
112112 }
113113
@@ -127,6 +127,10 @@ fn parse_gateway_metadata(path: &Path) -> Result<GatewayMetadata> {
127127 . into_diagnostic ( )
128128 . wrap_err ( "failed to parse gateway metadata" )
129129}
130+ fn user_entry_shadows_system ( metadata_path : & Path ) -> bool {
131+ metadata_path. try_exists ( ) . unwrap_or ( true )
132+ }
133+
130134/// Extract the hostname from an SSH destination string.
131135///
132136/// Handles formats like:
@@ -291,34 +295,63 @@ pub fn list_gateways_with_source() -> Result<Vec<ListedGateway>> {
291295 let mut gateways = Vec :: new ( ) ;
292296 let mut seen: std:: collections:: HashSet < String > = std:: collections:: HashSet :: new ( ) ;
293297
294- let mut scan = |dir : PathBuf , source : GatewayMetadataSource | -> Result < ( ) > {
295- if !dir. exists ( ) {
296- return Ok ( ( ) ) ;
298+ let user_dir = user_gateways_dir ( ) ?;
299+ if user_dir. exists ( ) {
300+ let entries = std:: fs:: read_dir ( & user_dir)
301+ . into_diagnostic ( )
302+ . wrap_err_with ( || format ! ( "failed to read directory {}" , user_dir. display( ) ) ) ?;
303+ for entry in entries {
304+ let entry = entry. into_diagnostic ( ) ?;
305+ let path = entry. path ( ) ;
306+ if !path. is_dir ( ) {
307+ continue ;
308+ }
309+
310+ let metadata_path = path. join ( "metadata.json" ) ;
311+ if !user_entry_shadows_system ( & metadata_path) {
312+ continue ;
313+ }
314+
315+ let name = entry. file_name ( ) . to_string_lossy ( ) . to_string ( ) ;
316+ if !seen. insert ( name) {
317+ continue ;
318+ }
319+
320+ if let Ok ( metadata) = parse_gateway_metadata ( & metadata_path) {
321+ gateways. push ( ListedGateway {
322+ metadata,
323+ source : GatewayMetadataSource :: User ,
324+ } ) ;
325+ }
297326 }
298- let entries = std:: fs:: read_dir ( & dir)
327+ }
328+
329+ let system_dir = system_gateways_dir ( ) ;
330+ if system_dir. exists ( ) {
331+ let entries = std:: fs:: read_dir ( & system_dir)
299332 . into_diagnostic ( )
300- . wrap_err_with ( || format ! ( "failed to read directory {}" , dir . display( ) ) ) ?;
333+ . wrap_err_with ( || format ! ( "failed to read directory {}" , system_dir . display( ) ) ) ?;
301334 for entry in entries {
302335 let entry = entry. into_diagnostic ( ) ?;
303336 let path = entry. path ( ) ;
304337 if !path. is_dir ( ) {
305338 continue ;
306339 }
340+
307341 let name = entry. file_name ( ) . to_string_lossy ( ) . to_string ( ) ;
308342 if seen. contains ( & name) {
309343 continue ;
310344 }
345+
311346 let metadata_path = path. join ( "metadata.json" ) ;
312347 if let Ok ( metadata) = parse_gateway_metadata ( & metadata_path) {
313- seen. insert ( name) ;
314- gateways. push ( ListedGateway { metadata, source } ) ;
348+ gateways. push ( ListedGateway {
349+ metadata,
350+ source : GatewayMetadataSource :: System ,
351+ } ) ;
315352 }
316353 }
317- Ok ( ( ) )
318- } ;
319-
320- scan ( user_gateways_dir ( ) ?, GatewayMetadataSource :: User ) ?;
321- scan ( system_gateways_dir ( ) , GatewayMetadataSource :: System ) ?;
354+ }
322355
323356 gateways. sort_by ( |a, b| a. metadata . name . cmp ( & b. metadata . name ) ) ;
324357 Ok ( gateways)
@@ -730,4 +763,49 @@ mod tests {
730763 assert_eq ! ( gateways[ 0 ] . source, GatewayMetadataSource :: User ) ;
731764 } ) ;
732765 }
766+
767+ #[ test]
768+ fn list_gateways_invalid_user_entry_still_shadows_system ( ) {
769+ let user = tempfile:: tempdir ( ) . unwrap ( ) ;
770+ let system = tempfile:: tempdir ( ) . unwrap ( ) ;
771+ with_tmp_xdg_and_system ( user. path ( ) , system. path ( ) , || {
772+ let user_metadata_path = user_gateway_metadata_path ( "shared" ) . unwrap ( ) ;
773+ std:: fs:: create_dir_all ( user_metadata_path. parent ( ) . unwrap ( ) ) . unwrap ( ) ;
774+ std:: fs:: write ( & user_metadata_path, "{not-json" ) . unwrap ( ) ;
775+
776+ write_system_metadata ( & system. path ( ) . join ( "gateways" ) , "shared" , "https://system" ) ;
777+
778+ let gateways = list_gateways_with_source ( ) . unwrap ( ) ;
779+ assert ! ( gateways. is_empty( ) ) ;
780+ } ) ;
781+ }
782+ #[ test]
783+ fn list_gateways_empty_user_dir_does_not_hide_system ( ) {
784+ let user = tempfile:: tempdir ( ) . unwrap ( ) ;
785+ let system = tempfile:: tempdir ( ) . unwrap ( ) ;
786+ with_tmp_xdg_and_system ( user. path ( ) , system. path ( ) , || {
787+ let user_meta = GatewayMetadata {
788+ name : "shared" . to_string ( ) ,
789+ gateway_endpoint : "https://user" . to_string ( ) ,
790+ ..Default :: default ( )
791+ } ;
792+ store_gateway_metadata ( "shared" , & user_meta) . unwrap ( ) ;
793+ remove_gateway_metadata ( "shared" ) . unwrap ( ) ;
794+
795+ let user_gateway_dir = user_gateway_metadata_path ( "shared" )
796+ . unwrap ( )
797+ . parent ( )
798+ . unwrap ( )
799+ . to_path_buf ( ) ;
800+ assert ! ( user_gateway_dir. is_dir( ) ) ;
801+ assert ! ( !user_gateway_dir. join( "metadata.json" ) . exists( ) ) ;
802+
803+ write_system_metadata ( & system. path ( ) . join ( "gateways" ) , "shared" , "https://system" ) ;
804+
805+ let gateways = list_gateways_with_source ( ) . unwrap ( ) ;
806+ assert_eq ! ( gateways. len( ) , 1 ) ;
807+ assert_eq ! ( gateways[ 0 ] . metadata. gateway_endpoint, "https://system" ) ;
808+ assert_eq ! ( gateways[ 0 ] . source, GatewayMetadataSource :: System ) ;
809+ } ) ;
810+ }
733811}
0 commit comments