use std::collections::HashMap;
#[derive(serde::Deserialize)]
struct RosString {
data: String,
}
pub(crate) fn extract_urdf_from_robot_descriptions(
mcap_bytes: &[u8],
summary: &mcap::Summary,
topic_filter: &re_mcap::TopicFilter,
recover: bool,
emit: &(dyn Fn(re_chunk::Chunk) + Send + Sync),
) -> anyhow::Result<()> {
let robot_desc_channels: Vec<u16> = summary
.channels
.values()
.filter(|channel| {
topic_filter.matches(&channel.topic)
&& channel.topic.contains("robot_description")
&& channel.schema.as_ref().is_some_and(|schema| {
schema.name == "std_msgs/msg/String" && schema.encoding == "ros2msg"
})
})
.map(|channel| channel.id)
.collect();
if robot_desc_channels.is_empty() {
return Ok(());
}
re_log::debug!(
"Found {} robot_description channel(s), scanning messages…",
robot_desc_channels.len()
);
let mut urdf_by_channel: HashMap<u16, String> = HashMap::new();
let messages = if recover {
mcap::MessageStream::new_with_options(
mcap_bytes,
mcap::read::Options::IgnoreEndMagic.into(),
)?
} else {
mcap::MessageStream::new(mcap_bytes)?
};
for msg in messages {
let msg = match msg {
Ok(msg) => msg,
Err(err) if recover => {
re_log::warn!("Stopping URDF scan at a truncated/corrupt MCAP tail: {err}");
break;
}
Err(err) => return Err(err.into()),
};
if robot_desc_channels.contains(&msg.channel.id)
&& let Ok(decoded) = re_mcap::cdr::try_decode_message::<RosString>(&msg.data)
{
urdf_by_channel.insert(msg.channel.id, decoded.data);
}
}
for urdf_xml in urdf_by_channel.into_values() {
match crate::importer_urdf::build_urdf_chunks_from_xml(
&urdf_xml,
None,
&re_log_types::TimePoint::STATIC,
false,
) {
Ok(chunks) => {
re_log::debug!(
"URDF extraction produced {} chunks from robot_description.",
chunks.len()
);
for chunk in chunks {
emit(chunk);
}
}
Err(err) => {
re_log::warn_once!("Failed to parse URDF from robot_description topic: {err}");
}
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use std::io::Cursor;
fn cdr_string_message(s: &str) -> Vec<u8> {
let mut payload = vec![0x00, 0x01, 0x00, 0x00]; let bytes = s.as_bytes();
payload.extend_from_slice(&(bytes.len() as u32 + 1).to_le_bytes());
payload.extend_from_slice(bytes);
payload.push(0); payload
}
#[test]
fn recover_urdf_survives_truncated_tail() {
const URDF: &str = r#"<robot name="r"><link name="base"><visual><geometry><box size="1 1 1"/></geometry></visual></link></robot>"#;
let cursor = Cursor::new(Vec::new());
let mut writer = mcap::Writer::new(cursor).expect("writer");
let schema_id = writer
.add_schema("std_msgs/msg/String", "ros2msg", b"string data")
.expect("schema");
let channel_id = writer
.add_channel(schema_id, "/robot_description", "cdr", &Default::default())
.expect("channel");
writer
.write_to_known_channel(
&mcap::records::MessageHeader {
channel_id,
sequence: 0,
log_time: 1,
publish_time: 1,
},
&cdr_string_message(URDF),
)
.expect("message");
writer.flush().expect("flush");
writer.finish().expect("finish");
let buffer = writer.into_inner().into_inner();
let footer = mcap::read::footer(&buffer).expect("footer");
let truncated = &buffer[..footer.summary_start as usize];
let summary = re_mcap::read_or_reconstruct_summary(truncated, true).expect("reconstruct");
let chunks = parking_lot::Mutex::new(Vec::new());
super::extract_urdf_from_robot_descriptions(
truncated,
&summary,
&re_mcap::TopicFilter::default(),
true, &|chunk| chunks.lock().push(chunk),
)
.expect("URDF extraction should not error on a truncated file in recover mode");
assert!(
!chunks.lock().is_empty(),
"expected URDF chunks to be emitted from the truncated file"
);
}
}