@@ -198,18 +198,55 @@ fn convert_base_type(
198198/// `None` if the type cannot be found or parsed.
199199#[ cfg( feature = "dynamic-schema-loader" ) ]
200200pub fn load_schema ( type_name : & str ) -> Option < Arc < MessageSchema > > {
201+ let in_progress = std:: cell:: RefCell :: new ( std:: collections:: HashSet :: new ( ) ) ;
202+ load_schema_inner ( type_name, & in_progress)
203+ }
204+
205+ /// Recursive worker for [`load_schema`]. `in_progress` tracks the types whose
206+ /// resolution is on the current stack so a self-referential or mutually
207+ /// recursive `.msg` (malformed — well-formed ROS messages form a DAG) bails
208+ /// with a warning instead of recursing until the stack overflows: a cycle would
209+ /// otherwise re-enter here for a type that isn't registered yet, so the
210+ /// `get_schema` memo never hits.
211+ #[ cfg( feature = "dynamic-schema-loader" ) ]
212+ fn load_schema_inner (
213+ type_name : & str ,
214+ in_progress : & std:: cell:: RefCell < std:: collections:: HashSet < String > > ,
215+ ) -> Option < Arc < MessageSchema > > {
201216 if let Some ( schema) = get_schema ( type_name) {
202217 return Some ( schema) ;
203218 }
204219 let ( package, name) = split_msg_type ( type_name) ?;
220+ // File not on disk is a legitimate "try the next source" (live discovery),
221+ // so return None quietly. Errors *after* a file is found are logged below,
222+ // since a broken `.msg` masquerading as "not found" would be misleading.
205223 let path = find_msg_file ( & package, & name) ?;
206- let parsed = hiroz_codegen:: parser:: msg:: parse_msg_file ( & path, & package) . ok ( ) ?;
224+ if !in_progress. borrow_mut ( ) . insert ( type_name. to_string ( ) ) {
225+ tracing:: warn!( "cyclic .msg definition for {type_name}; skipping schema load" ) ;
226+ return None ;
227+ }
228+ let parsed = match hiroz_codegen:: parser:: msg:: parse_msg_file ( & path, & package) {
229+ Ok ( parsed) => parsed,
230+ Err ( e) => {
231+ tracing:: warn!( "failed to parse .msg file {}: {e}" , path. display( ) ) ;
232+ in_progress. borrow_mut ( ) . remove ( type_name) ;
233+ return None ;
234+ }
235+ } ;
207236 // Resolve nested message-typed fields by loading them the same way; each
208237 // recursive load registers itself, so the outer conversion sees them.
209238 let resolver = |field_pkg : & str , field_type : & str | -> Option < Arc < MessageSchema > > {
210- load_schema ( & format ! ( "{field_pkg}/msg/{field_type}" ) )
239+ load_schema_inner ( & format ! ( "{field_pkg}/msg/{field_type}" ) , in_progress)
240+ } ;
241+ let schema = match parsed_message_to_schema ( & parsed, & resolver) {
242+ Ok ( schema) => schema,
243+ Err ( e) => {
244+ tracing:: warn!( "failed to build schema for {type_name}: {e}" ) ;
245+ in_progress. borrow_mut ( ) . remove ( type_name) ;
246+ return None ;
247+ }
211248 } ;
212- let schema = parsed_message_to_schema ( & parsed , & resolver ) . ok ( ) ? ;
249+ in_progress . borrow_mut ( ) . remove ( type_name ) ;
213250 Some ( register_schema ( schema) )
214251}
215252
@@ -225,9 +262,12 @@ fn split_msg_type(type_name: &str) -> Option<(String, String)> {
225262}
226263
227264/// Find `<pkg>/msg/<Name>.msg` under `HIROZ_MSG_PATH`. Each colon-separated
228- /// entry is tried both as the package directory itself
229- /// (`<entry>/msg/<Name>.msg`) and as a prefix that contains packages
230- /// (`<entry>/<pkg>/msg/<Name>.msg`, e.g. an ament `.../share`).
265+ /// entry is tried as a prefix that contains packages
266+ /// (`<entry>/<pkg>/msg/<Name>.msg`, e.g. an ament `.../share`), and — only when
267+ /// the entry's own basename equals `pkg` — as the package directory itself
268+ /// (`<entry>/msg/<Name>.msg`). The basename guard is what keeps a request for
269+ /// `pkg_a/msg/Status` from silently resolving to an unrelated
270+ /// `pkg_b/msg/Status.msg` that happens to appear earlier in the path.
231271#[ cfg( feature = "dynamic-schema-loader" ) ]
232272fn find_msg_file ( package : & str , name : & str ) -> Option < std:: path:: PathBuf > {
233273 let msg_path = std:: env:: var ( "HIROZ_MSG_PATH" ) . ok ( ) ?;
@@ -238,14 +278,18 @@ fn find_msg_file(package: &str, name: &str) -> Option<std::path::PathBuf> {
238278 continue ;
239279 }
240280 let base = std:: path:: Path :: new ( entry) ;
241- let as_package = base. join ( "msg" ) . join ( & file) ;
242- if as_package. is_file ( ) {
243- return Some ( as_package) ;
244- }
281+ // Prefix layout: `package` is part of the path, so it can't cross packages.
245282 let as_prefix = base. join ( package) . join ( "msg" ) . join ( & file) ;
246283 if as_prefix. is_file ( ) {
247284 return Some ( as_prefix) ;
248285 }
286+ // Package-directory layout: valid only if this entry IS the package's dir.
287+ if base. file_name ( ) . and_then ( |n| n. to_str ( ) ) == Some ( package) {
288+ let as_package = base. join ( "msg" ) . join ( & file) ;
289+ if as_package. is_file ( ) {
290+ return Some ( as_package) ;
291+ }
292+ }
249293 }
250294 None
251295}
0 commit comments