rs-bag 0.1.0

Read, write and modify ROS 2 rosbag2 sqlite3 (.db3) storage files
Documentation
use std::collections::HashSet;

use serde::{Deserialize, Serialize};

use crate::{Result, Topic, storage::Db3File};

#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub struct Duration {
    pub nanoseconds: i64,
}

impl Duration {
    pub fn new(nanoseconds: i64) -> Self {
        Self { nanoseconds }
    }
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub struct StartingTime {
    pub nanoseconds_since_epoch: i64,
}

impl StartingTime {
    pub fn new(nanoseconds_since_epoch: i64) -> Self {
        Self {
            nanoseconds_since_epoch,
        }
    }
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct TopicMetadata {
    pub name: String,
    #[serde(rename = "type")]
    pub r#type: String,
    pub serialization_format: String,
    pub offered_qos_profiles: String,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct TopicWithMessageCount {
    pub topic_metadata: TopicMetadata,
    pub message_count: u64,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct BagFile {
    pub path: String,
    pub starting_time: StartingTime,
    pub duration: Duration,
    pub message_count: u64,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct BagfileInformation {
    pub version: u32,
    pub storage_identifier: String,
    pub duration: Duration,
    pub starting_time: StartingTime,
    pub message_count: u64,
    pub topics_with_message_count: Vec<TopicWithMessageCount>,
    pub compression_format: String,
    pub compression_mode: String,
    pub relative_file_paths: Vec<String>,
    pub files: Vec<BagFile>,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Metadata {
    pub rosbag2_bagfile_information: BagfileInformation,
    #[serde(skip)]
    pub path: std::path::PathBuf,
}

impl Metadata {
    pub fn create<'a, P: AsRef<std::path::Path>>(
        topics: &HashSet<Topic>,
        db3_files: impl Iterator<Item = &'a Db3File>,
        path: P,
    ) -> Result<Self> {
        todo!("Assemble BagfileInformation from topics and db3 files")
    }

    pub fn save<P: AsRef<std::path::Path>>(&self, path: P) -> Result<()> {
        todo!("Serialize metadata to YAML and write it to the given path")
    }

    pub fn to_yaml(&self) -> Result<String> {
        Ok(serde_yaml::to_string(self)?)
    }

    pub fn topics(&self) -> &[TopicWithMessageCount] {
        &self.rosbag2_bagfile_information.topics_with_message_count
    }

    pub fn add_topic(&self, topic: &Topic, message_count: i64) -> Result<()> {
        todo!("Append a TopicWithMessageCount entry to topics_with_message_count")
    }
}

impl BagfileInformation {
    pub fn find_topic(&self, name: &str) -> Option<&TopicMetadata> {
        self.topics_with_message_count
            .iter()
            .find(|t| t.topic_metadata.name == name)
            .map(|t| &t.topic_metadata)
    }
}

pub const DEFAULT_OFFERED_QOS_PROFILES: &str = "\
- history: 3\n\
  depth: 0\n\
  reliability: 1\n\
  durability: 2\n\
  deadline:\n\
    sec: 9223372036\n\
    nsec: 854775807\n\
  lifespan:\n\
    sec: 9223372036\n\
    nsec: 854775807\n\
  liveliness: 1\n\
  liveliness_lease_duration:\n\
    sec: 9223372036\n\
    nsec: 854775807\n\
  avoid_ros_namespace_conventions: false";

// #[cfg(test)]
// mod tests {
//     use super::*;

//     const REFERENCE_YAML: &str = "../rosbag2_2026_07_14-23_54_05/metadata.yaml";

//     #[test]
//     fn parse_reference_metadata() {
//         let path = std::path::Path::new(REFERENCE_YAML);
//         if !path.exists() {
//             // Reference bag isn't checked out alongside this crate in every
//             // checkout; skip rather than fail.
//             eprintln!("skipping: {REFERENCE_YAML} not present");
//             return;
//         }
//         let md = Metadata::from_path(path).expect("parse reference metadata.yaml");
//         let info = &md.rosbag2_bagfile_information;
//         assert_eq!(info.version, 5);
//         assert_eq!(info.storage_identifier, "sqlite3");
//         assert_eq!(info.message_count, 455);
//         assert_eq!(info.duration, Duration::new(5550152856));
//         assert_eq!(info.starting_time, StartingTime::new(1784066045789306347));
//         assert_eq!(info.topics_with_message_count.len(), 8);
//         assert_eq!(
//             info.relative_file_paths,
//             vec!["rosbag2_2026_07_14-23_54_05_0.db3"]
//         );
//         assert_eq!(info.files.len(), 1);

//         // Find a known topic and check the carried-over QoS string.
//         let image = info
//             .find_topic("/image_raw")
//             .expect("/image_raw topic present");
//         assert_eq!(image.r#type, "sensor_msgs/msg/Image");
//         assert_eq!(image.serialization_format, "cdr");
//         assert!(image.offered_qos_profiles.contains("reliability: 1"));
//     }

//     #[test]
//     fn round_trip_yaml() {
//         let yaml = r#"
// rosbag2_bagfile_information:
//   version: 5
//   storage_identifier: sqlite3
//   duration:
//     nanoseconds: 1000
//   starting_time:
//     nanoseconds_since_epoch: 42
//   message_count: 2
//   topics_with_message_count:
//     - topic_metadata:
//         name: /foo
//         type: std_msgs/msg/String
//         serialization_format: cdr
//         offered_qos_profiles: "- history: 1\n  depth: 1"
//       message_count: 2
//   compression_format: ""
//   compression_mode: ""
//   relative_file_paths:
//     - bag_0.db3
//   files:
//     - path: bag_0.db3
//       starting_time:
//         nanoseconds_since_epoch: 42
//       duration:
//         nanoseconds: 1000
//       message_count: 2
// "#;
//         let md = Metadata::from_str(yaml).expect("parse");
//         let out = md.to_yaml().expect("serialize");
//         let md2 = Metadata::from_str(&out).expect("reparse");
//         assert_eq!(md, md2);
//     }
// }