-
Notifications
You must be signed in to change notification settings - Fork 221
rust cli: Implement mcap get commands
#1622
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
8107bcf
44d8d90
94f7ef2
384eddd
b7fdd9c
40def26
ede5524
d283883
6ae8332
425675b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,8 +1,139 @@ | ||
| use anyhow::Result; | ||
| use std::io::IsTerminal as _; | ||
| use std::io::Write as _; | ||
|
|
||
| use crate::commands::not_implemented; | ||
| use anyhow::{Context, Result}; | ||
|
|
||
| use crate::cli::GetAttachmentCommand; | ||
| use crate::commands::common; | ||
| use crate::context::CommandContext; | ||
|
|
||
| pub fn run(_ctx: &CommandContext) -> Result<()> { | ||
| Err(not_implemented("get attachment")) | ||
| const PLEASE_REDIRECT: &str = | ||
| "Binary output can screw up your terminal. Supply -o or redirect to a file or pipe"; | ||
|
|
||
| pub fn run(_ctx: &CommandContext, args: GetAttachmentCommand) -> Result<()> { | ||
| let mcap = common::map_file(&args.file)?; | ||
| let parsed = common::parse_mcap(&mcap)?; | ||
| let index = select_attachment_index(&parsed.attachment_indexes, &args.name, args.offset)?; | ||
| let attachment = mcap::read::attachment(&mcap, index).with_context(|| { | ||
| format!( | ||
| "failed to read attachment {} at offset {}", | ||
| args.name, index.offset | ||
| ) | ||
| })?; | ||
|
|
||
| if let Some(output) = args.output { | ||
| std::fs::write(&output, &attachment.data) | ||
| .with_context(|| format!("failed to write attachment to '{}'", output.display()))?; | ||
| } else if std::io::stdout().is_terminal() { | ||
| anyhow::bail!("{PLEASE_REDIRECT}"); | ||
| } else { | ||
| std::io::stdout() | ||
| .write_all(&attachment.data) | ||
| .context("failed to write attachment to stdout")?; | ||
| } | ||
|
|
||
| Ok(()) | ||
| } | ||
|
|
||
| fn select_attachment_index<'a>( | ||
| indexes: &'a [mcap::records::AttachmentIndex], | ||
| name: &str, | ||
| offset: Option<u64>, | ||
| ) -> Result<&'a mcap::records::AttachmentIndex> { | ||
| let matches: Vec<&mcap::records::AttachmentIndex> = | ||
| indexes.iter().filter(|index| index.name == name).collect(); | ||
|
|
||
| match matches.len() { | ||
| 0 => anyhow::bail!("attachment {name} not found"), | ||
| 1 => { | ||
| let first_match = matches[0]; | ||
| if let Some(offset) = offset { | ||
| if first_match.offset != offset { | ||
| anyhow::bail!("failed to find attachment {name} at offset {offset}"); | ||
| } | ||
| } | ||
| Ok(first_match) | ||
|
Comment on lines
+48
to
+55
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nit: the Rust CLI now validates
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Rust behavior is better |
||
| } | ||
| _ => { | ||
| let offset = offset.ok_or_else(|| { | ||
| anyhow::anyhow!("multiple attachments named {name} exist (specify an offset)") | ||
| })?; | ||
|
|
||
| matches | ||
| .into_iter() | ||
| .find(|index| index.offset == offset) | ||
| .ok_or_else(|| { | ||
| anyhow::anyhow!("failed to find attachment {name} at offset {offset}") | ||
| }) | ||
| } | ||
| } | ||
| } | ||
|
|
||
| #[cfg(test)] | ||
| mod tests { | ||
| use super::select_attachment_index; | ||
| use mcap::records::AttachmentIndex; | ||
|
|
||
| fn attachment(name: &str, offset: u64) -> AttachmentIndex { | ||
| AttachmentIndex { | ||
| offset, | ||
| length: 1, | ||
| log_time: 0, | ||
| create_time: 0, | ||
| data_size: 1, | ||
| name: name.to_string(), | ||
| media_type: "application/octet-stream".to_string(), | ||
| } | ||
| } | ||
|
|
||
| #[test] | ||
| fn selects_single_match_without_offset() { | ||
| let indexes = vec![attachment("a", 10)]; | ||
| let selected = | ||
| select_attachment_index(&indexes, "a", None).expect("attachment should resolve"); | ||
| assert_eq!(selected.offset, 10); | ||
| } | ||
|
|
||
| #[test] | ||
| fn errors_when_name_not_found() { | ||
| let indexes = vec![attachment("a", 10)]; | ||
| let err = select_attachment_index(&indexes, "b", None) | ||
| .expect_err("missing attachment should error"); | ||
| assert_eq!(err.to_string(), "attachment b not found"); | ||
| } | ||
|
|
||
| #[test] | ||
| fn errors_when_duplicate_without_offset() { | ||
| let indexes = vec![attachment("a", 10), attachment("a", 20)]; | ||
| let err = select_attachment_index(&indexes, "a", None) | ||
| .expect_err("duplicate attachments need offset"); | ||
| assert_eq!( | ||
| err.to_string(), | ||
| "multiple attachments named a exist (specify an offset)" | ||
| ); | ||
| } | ||
|
|
||
| #[test] | ||
| fn resolves_duplicate_with_matching_offset() { | ||
| let indexes = vec![attachment("a", 10), attachment("a", 20)]; | ||
| let selected = | ||
| select_attachment_index(&indexes, "a", Some(20)).expect("offset should disambiguate"); | ||
| assert_eq!(selected.offset, 20); | ||
| } | ||
|
|
||
| #[test] | ||
| fn errors_when_duplicate_offset_missing() { | ||
| let indexes = vec![attachment("a", 10), attachment("a", 20)]; | ||
| let err = select_attachment_index(&indexes, "a", Some(999)) | ||
| .expect_err("unknown offset should error"); | ||
| assert_eq!(err.to_string(), "failed to find attachment a at offset 999"); | ||
| } | ||
|
|
||
| #[test] | ||
| fn errors_when_single_match_has_different_offset() { | ||
| let indexes = vec![attachment("a", 10)]; | ||
| let err = select_attachment_index(&indexes, "a", Some(999)) | ||
| .expect_err("single record should enforce provided offset"); | ||
| assert_eq!(err.to_string(), "failed to find attachment a at offset 999"); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,8 +1,111 @@ | ||
| use anyhow::Result; | ||
| use std::collections::BTreeMap; | ||
|
|
||
| use crate::commands::not_implemented; | ||
| use anyhow::{Context, Result}; | ||
|
|
||
| use crate::cli::GetMetadataCommand; | ||
| use crate::commands::common; | ||
| use crate::context::CommandContext; | ||
|
|
||
| pub fn run(_ctx: &CommandContext) -> Result<()> { | ||
| Err(not_implemented("get metadata")) | ||
| pub fn run(_ctx: &CommandContext, args: GetMetadataCommand) -> Result<()> { | ||
| let mcap = common::map_file(&args.file)?; | ||
| let parsed = common::parse_mcap(&mcap)?; | ||
| let metadata = merged_metadata_for_name(&mcap, &parsed.metadata_indexes, &args.name)?; | ||
| let pretty = | ||
| serde_json::to_string_pretty(&metadata).context("failed to serialize metadata to JSON")?; | ||
| println!("{pretty}"); | ||
| Ok(()) | ||
| } | ||
|
|
||
| fn merged_metadata_for_name( | ||
| mcap: &[u8], | ||
| indexes: &[mcap::records::MetadataIndex], | ||
| name: &str, | ||
| ) -> Result<BTreeMap<String, String>> { | ||
| let mut matching_indexes: Vec<&mcap::records::MetadataIndex> = | ||
| indexes.iter().filter(|index| index.name == name).collect(); | ||
| if matching_indexes.is_empty() { | ||
| anyhow::bail!("metadata {name} does not exist"); | ||
| } | ||
| matching_indexes.sort_by_key(|index| index.offset); | ||
|
|
||
| let mut output = BTreeMap::new(); | ||
| for index in matching_indexes { | ||
| let record = mcap::read::metadata(mcap, index) | ||
| .with_context(|| format!("failed to read metadata at offset {}", index.offset))?; | ||
| for (key, value) in record.metadata { | ||
| output.insert(key, value); | ||
| } | ||
| } | ||
| Ok(output) | ||
| } | ||
|
|
||
| #[cfg(test)] | ||
| mod tests { | ||
| use std::collections::BTreeMap; | ||
|
|
||
| use mcap::records::MetadataIndex; | ||
|
|
||
| use super::merged_metadata_for_name; | ||
|
|
||
| fn metadata_index(name: &str, offset: u64, length: u64) -> MetadataIndex { | ||
| MetadataIndex { | ||
| offset, | ||
| length, | ||
| name: name.to_string(), | ||
| } | ||
| } | ||
|
|
||
| #[test] | ||
| fn errors_when_metadata_name_missing() { | ||
| let err = merged_metadata_for_name(&[], &[metadata_index("demo", 0, 0)], "other") | ||
| .expect_err("missing metadata should fail"); | ||
| assert_eq!(err.to_string(), "metadata other does not exist"); | ||
| } | ||
|
|
||
| #[test] | ||
| fn merges_metadata_records_by_offset_order() { | ||
| let mut mcap_bytes = Vec::new(); | ||
| let (first, second) = { | ||
| let mut writer = mcap::WriteOptions::new() | ||
| .emit_metadata_indexes(true) | ||
| .emit_summary_records(true) | ||
| .emit_summary_offsets(true) | ||
| .create(std::io::Cursor::new(&mut mcap_bytes)) | ||
| .expect("writer"); | ||
| writer | ||
| .write_metadata(&mcap::records::Metadata { | ||
| name: "config".to_string(), | ||
| metadata: BTreeMap::from([ | ||
| ("a".to_string(), "1".to_string()), | ||
| ("b".to_string(), "1".to_string()), | ||
| ]), | ||
| }) | ||
| .expect("first metadata"); | ||
| writer | ||
| .write_metadata(&mcap::records::Metadata { | ||
| name: "config".to_string(), | ||
| metadata: BTreeMap::from([ | ||
| ("b".to_string(), "2".to_string()), | ||
| ("c".to_string(), "3".to_string()), | ||
| ]), | ||
| }) | ||
| .expect("second metadata"); | ||
| let summary = writer.finish().expect("finish"); | ||
| let mut indexes: Vec<MetadataIndex> = summary.metadata_indexes; | ||
| indexes.sort_by_key(|index| index.offset); | ||
| (indexes[0].clone(), indexes[1].clone()) | ||
| }; | ||
|
|
||
| let latest = | ||
| merged_metadata_for_name(&mcap_bytes, &[second.clone(), first.clone()], "config") | ||
| .expect("metadata should merge"); | ||
| assert_eq!( | ||
| latest, | ||
| BTreeMap::from([ | ||
| ("a".to_string(), "1".to_string()), | ||
| ("b".to_string(), "2".to_string()), | ||
| ("c".to_string(), "3".to_string()), | ||
| ]) | ||
| ); | ||
|
claude[bot] marked this conversation as resolved.
|
||
| } | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.