mcap_rs/
lib.rs

1//! A library for manipulating [Foxglove MCAP](https://github.com/foxglove/mcap) files,
2//! both reading:
3//!
4//! ```no_run
5//! use std::fs;
6//!
7//! use anyhow::{Context, Result};
8//! use camino::Utf8Path;
9//! use memmap::Mmap;
10//!
11//! fn map_mcap<P: AsRef<Utf8Path>>(p: P) -> Result<Mmap> {
12//!     let fd = fs::File::open(p.as_ref()).context("Couldn't open MCAP file")?;
13//!     unsafe { Mmap::map(&fd) }.context("Couldn't map MCAP file")
14//! }
15//!
16//! fn read_it() -> Result<()> {
17//!     let mapped = map_mcap("in.mcap")?;
18//!
19//!     for message in mcap_rs::MessageStream::new(&mapped)? {
20//!         println!("{:?}", message?);
21//!         // Or whatever else you'd like to do...
22//!     }
23//!     Ok(())
24//! }
25//! ```
26//! or writing:
27//! ```no_run
28//! use std::{collections::BTreeMap, fs, io::BufWriter};
29//!
30//! use anyhow::Result;
31//!
32//! use mcap_rs::{Channel, records::MessageHeader, Writer};
33//!
34//! fn write_it() -> Result<()> {
35//!     // To set the profile or compression options, see mcap_rs::WriteOptions.
36//!     let mut out = Writer::new(
37//!         BufWriter::new(fs::File::create("out.mcap")?)
38//!     )?;
39//!
40//!     // Channels and schemas are automatically assigned ID as they're serialized,
41//!     // and automatically deduplicated with `Arc` when deserialized.
42//!     let my_channel = Channel {
43//!         topic: String::from("cool stuff"),
44//!         schema: None,
45//!         message_encoding: String::from("application/octet-stream"),
46//!         metadata: BTreeMap::default()
47//!     };
48//!
49//!     let channel_id = out.add_channel(&my_channel)?;
50//!
51//!     out.write_to_known_channel(
52//!         &MessageHeader {
53//!             channel_id,
54//!             sequence: 25,
55//!             log_time: 6,
56//!             publish_time: 24
57//!         },
58//!         &[1, 2, 3]
59//!     )?;
60//!     out.write_to_known_channel(
61//!         &MessageHeader {
62//!             channel_id,
63//!             sequence: 32,
64//!             log_time: 23,
65//!             publish_time: 25
66//!         },
67//!         &[3, 4, 5]
68//!     )?;
69//!
70//!     out.finish()?;
71//!
72//!     Ok(())
73//! }
74//! ```
75
76pub mod read;
77pub mod records;
78pub mod write;
79
80mod io_utils;
81
82use std::{borrow::Cow, collections::BTreeMap, fmt, sync::Arc};
83
84use thiserror::Error;
85
86#[derive(Debug, Error)]
87pub enum McapError {
88    #[error("Bad magic number")]
89    BadMagic,
90    #[error("Footer record couldn't be found at the end of the file, before the magic bytes")]
91    BadFooter,
92    #[error("Attachment CRC failed (expeted {saved:08X}, got {calculated:08X}")]
93    BadAttachmentCrc { saved: u32, calculated: u32 },
94    #[error("Chunk CRC failed (expected {saved:08X}, got {calculated:08X}")]
95    BadChunkCrc { saved: u32, calculated: u32 },
96    #[error("Data section CRC failed (expected {saved:08X}, got {calculated:08X})")]
97    BadDataCrc { saved: u32, calculated: u32 },
98    #[error("Summary section CRC failed (expected {saved:08X}, got {calculated:08X})")]
99    BadSummaryCrc { saved: u32, calculated: u32 },
100    #[error("Index offset and length didn't point to the expected record type")]
101    BadIndex,
102    #[error("Channel `{0}` has mulitple records that don't match.")]
103    ConflictingChannels(String),
104    #[error("Schema `{0}` has mulitple records that don't match.")]
105    ConflictingSchemas(String),
106    #[error("Record parse failed")]
107    Parse(#[from] binrw::Error),
108    #[error("I/O error from writing, or reading a compression stream")]
109    Io(#[from] std::io::Error),
110    #[error("Schema has an ID of 0")]
111    InvalidSchemaId,
112    #[error("MCAP file ended in the middle of a record")]
113    UnexpectedEof,
114    #[error("Chunk ended in the middle of a record")]
115    UnexpectedEoc,
116    #[error("Message {0} referenced unknown channel {1}")]
117    UnknownChannel(u32, u16),
118    #[error("Channel `{0}` referenced unknown schema {1}")]
119    UnknownSchema(String, u16),
120    #[error("Found record with opcode {0:02X} in a chunk")]
121    UnexpectedChunkRecord(u8),
122    #[error("Unsupported compression format `{0}`")]
123    UnsupportedCompression(String),
124}
125
126pub type McapResult<T> = Result<T, McapError>;
127
128/// Magic bytes for the MCAP format
129pub const MAGIC: &[u8] = &[0x89, b'M', b'C', b'A', b'P', 0x30, b'\r', b'\n'];
130
131/// Compression options for chunks of channels, schemas, and messages in an MCAP file
132#[derive(Debug, Copy, Clone, Default)]
133pub enum Compression {
134    #[default]
135    Zstd,
136    Lz4,
137}
138
139/// Describes a schema used by one or more [Channel]s in an MCAP file
140///
141/// The [`CoW`](std::borrow::Cow) can either borrow directly from the mapped file,
142/// or hold its own buffer if it was decompressed from a chunk.
143#[derive(Clone, PartialEq, Eq, Hash)]
144pub struct Schema<'a> {
145    pub name: String,
146    pub encoding: String,
147    pub data: Cow<'a, [u8]>,
148}
149
150impl fmt::Debug for Schema<'_> {
151    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
152        f.debug_struct("Schema")
153            .field("name", &self.name)
154            .field("encoding", &self.encoding)
155            .finish_non_exhaustive()
156    }
157}
158
159/// Describes a channel which [Message]s are published to in an MCAP file
160#[derive(Debug, Clone, PartialEq, Eq, Hash)]
161pub struct Channel<'a> {
162    pub topic: String,
163    pub schema: Option<Arc<Schema<'a>>>,
164
165    pub message_encoding: String,
166    pub metadata: BTreeMap<String, String>,
167}
168
169/// An event in an MCAP file, published to a [Channel]
170///
171/// The [`CoW`](std::borrow::Cow) can either borrow directly from the mapped file,
172/// or hold its own buffer if it was decompressed from a chunk.
173#[derive(Debug, Clone, PartialEq, Eq)]
174pub struct Message<'a> {
175    pub channel: Arc<Channel<'a>>,
176    pub sequence: u32,
177    pub log_time: u64,
178    pub publish_time: u64,
179    pub data: Cow<'a, [u8]>,
180}
181
182/// An attachment and its metadata in an MCAP file
183#[derive(Debug, PartialEq, Eq)]
184pub struct Attachment<'a> {
185    pub log_time: u64,
186    pub create_time: u64,
187    pub name: String,
188    pub content_type: String,
189    pub data: Cow<'a, [u8]>,
190}
191
192pub use read::{MessageStream, Summary};
193pub use write::{WriteOptions, Writer};