1use super::{Integer, Number, annotation, capture, global, utils::hash::hash_from_slice};
20use geojson::GeoJson;
21use serde_derive::{Deserialize, Serialize};
22use serde_json::{Map, Value};
23use sha2::{Digest, Sha512};
24use std::io::Read;
25
26#[derive(Debug, Clone, Deserialize, Serialize)]
55pub struct Metadata {
56 global: global::Global,
57 annotations: Vec<annotation::Annotation>,
58 captures: Vec<capture::Capture>,
59 #[serde(flatten)]
60 extra_fields: Map<String, Value>,
61 #[serde(skip)]
62 original_bytes: Option<Vec<u8>>,
63}
64
65impl Metadata {
66 pub fn new(
89 global: global::Global,
90 annotations: Vec<annotation::Annotation>,
91 captures: Vec<capture::Capture>,
92 ) -> Self {
93 Metadata {
94 global,
95 annotations,
96 captures,
97 extra_fields: Map::new(),
98 original_bytes: None,
99 }
100 }
101 pub fn from_slice(bytes: &[u8]) -> Result<Self, crate::SigmfError> {
148 if bytes.len() > crate::MAX_META_SIZE {
149 return Err(crate::SigmfError::ResourceLimit(format!(
150 "metadata exceeds the {} byte limit",
151 crate::MAX_META_SIZE
152 )));
153 }
154 let value = crate::json::parse_unique_value(bytes)?;
155 Self::validate_metadata_shape(&value)?;
156 let mut metadata: Self = serde_json::from_value(value)
157 .map_err(|error| crate::SigmfError::InvalidMetadata(error.to_string()))?;
158 metadata.original_bytes = Some(bytes.to_vec());
159 Ok(metadata)
160 }
161
162 pub fn to_vec(&self) -> Result<Vec<u8>, crate::SigmfError> {
172 Ok(serde_json::to_vec(self)?)
173 }
174
175 pub fn to_vec_pretty(&self) -> Result<Vec<u8>, crate::SigmfError> {
181 Ok(serde_json::to_vec_pretty(self)?)
182 }
183
184 pub fn write_to(
198 &self,
199 mut writer: impl std::io::Write,
200 policy: crate::validation::ValidationPolicy,
201 ) -> Result<String, crate::SigmfError> {
202 let report = self.validate(policy);
203 if report.status == crate::validation::ValidationStatus::Failed {
204 return Err(crate::SigmfError::InvalidMetadata(
205 "metadata failed validation and was not serialized".to_string(),
206 ));
207 }
208 let bytes = self.to_vec()?;
209 writer.write_all(&bytes)?;
210 hash_from_slice(&bytes)
211 }
212
213 pub fn save_new(
228 &self,
229 path: impl AsRef<std::path::Path>,
230 policy: crate::validation::ValidationPolicy,
231 ) -> Result<String, crate::SigmfError> {
232 self.save_atomic(path.as_ref(), policy, false)
233 }
234
235 pub fn replace_atomic(
249 &self,
250 path: impl AsRef<std::path::Path>,
251 policy: crate::validation::ValidationPolicy,
252 ) -> Result<String, crate::SigmfError> {
253 self.save_atomic(path.as_ref(), policy, true)
254 }
255
256 fn save_atomic(
257 &self,
258 path: &std::path::Path,
259 policy: crate::validation::ValidationPolicy,
260 replace: bool,
261 ) -> Result<String, crate::SigmfError> {
262 let report = self.validate(policy);
263 if report.status == crate::validation::ValidationStatus::Failed {
264 return Err(crate::SigmfError::InvalidMetadata(
265 "metadata failed validation and was not published".to_string(),
266 ));
267 }
268 let bytes = self.to_vec()?;
269 let digest = hash_from_slice(&bytes)?;
270 crate::persistence::write_atomic(path, &bytes, replace)?;
271 Ok(digest)
272 }
273
274 pub fn from_reader(reader: impl std::io::Read) -> Result<Self, crate::SigmfError> {
286 let mut bytes = Vec::new();
287 reader
288 .take(crate::MAX_META_SIZE as u64 + 1)
289 .read_to_end(&mut bytes)?;
290 if bytes.len() > crate::MAX_META_SIZE {
291 return Err(crate::SigmfError::InvalidMetadata(format!(
292 "metadata exceeds the {} byte limit",
293 crate::MAX_META_SIZE
294 )));
295 }
296 Self::from_slice(&bytes)
297 }
298
299 pub fn from_path(path: impl AsRef<std::path::Path>) -> Result<Self, crate::SigmfError> {
307 Self::from_reader(std::fs::File::open(path)?)
308 }
309
310 pub fn original_bytes(&self) -> Option<&[u8]> {
317 self.original_bytes.as_deref()
318 }
319
320 pub fn original_digest(&self) -> Result<Option<String>, crate::SigmfError> {
332 self.original_bytes
333 .as_deref()
334 .map(hash_from_slice)
335 .transpose()
336 }
337
338 pub fn hash(&self) -> Result<String, crate::SigmfError> {
349 let serialized = self.to_vec()?;
350 hash_from_slice(&serialized)
351 }
352
353 fn revision_fingerprint(&self) -> String {
354 let digest = Sha512::digest(format!("{self:?}").as_bytes());
355 digest.iter().map(|byte| format!("{byte:02x}")).collect()
356 }
357
358 pub fn global(&self) -> &global::Global {
360 &self.global
361 }
362
363 pub fn captures(&self) -> &[capture::Capture] {
365 &self.captures
366 }
367
368 pub fn annotations(&self) -> &[annotation::Annotation] {
370 &self.annotations
371 }
372
373 pub fn extra_fields(&self) -> &Map<String, Value> {
376 &self.extra_fields
377 }
378
379 pub fn capture_at(&self, sample_index: Integer) -> Option<&capture::Capture> {
387 self.captures
388 .iter()
389 .filter(|capture| capture.sample_start <= sample_index)
390 .max_by_key(|capture| capture.sample_start)
391 }
392
393 pub fn annotations_intersecting_samples(
406 &self,
407 start: Integer,
408 sample_count: Integer,
409 ) -> Result<Vec<&annotation::Annotation>, crate::SigmfError> {
410 let end = start
411 .checked_add(sample_count)
412 .ok_or_else(|| crate::SigmfError::InvalidRange("sample range end overflows".into()))?;
413 if sample_count == 0 {
414 return Ok(Vec::new());
415 }
416 let mut matches = Vec::new();
417 for annotation in &self.annotations {
418 let Some(count) = annotation.sample_count else {
419 continue;
420 };
421 if count == 0 {
422 continue;
423 }
424 let annotation_end = annotation.sample_start.checked_add(count).ok_or_else(|| {
425 crate::SigmfError::InvalidRange(format!(
426 "annotation at sample {} has an overflowing range",
427 annotation.sample_start
428 ))
429 })?;
430 if annotation.sample_start < end && annotation_end > start {
431 matches.push(annotation);
432 }
433 }
434 Ok(matches)
435 }
436
437 pub fn annotations_intersecting_frequency(
449 &self,
450 lower: Number,
451 upper: Number,
452 ) -> Result<Vec<&annotation::Annotation>, crate::SigmfError> {
453 if !lower.is_finite() || !upper.is_finite() || lower > upper {
454 return Err(crate::SigmfError::InvalidRange(
455 "frequency interval must be finite and ordered".into(),
456 ));
457 }
458 Ok(self
459 .annotations
460 .iter()
461 .filter(|annotation| {
462 matches!(
463 (annotation.freq_lower_edge, annotation.freq_upper_edge),
464 (Some(annotation_lower), Some(annotation_upper))
465 if annotation_lower <= upper && annotation_upper >= lower
466 )
467 })
468 .collect())
469 }
470
471 pub fn diff(&self, other: &Self) -> Result<Vec<MetadataChange>, crate::SigmfError> {
485 let before = serde_json::to_value(self)?;
486 let after = serde_json::to_value(other)?;
487 let mut changes = Vec::new();
488 collect_changes("", Some(&before), Some(&after), &mut changes);
489 Ok(changes)
490 }
491
492 pub fn insert_extra(
503 &mut self,
504 key: impl Into<String>,
505 value: Value,
506 ) -> Result<Option<Value>, crate::SigmfError> {
507 let key = key.into();
508 if matches!(key.as_str(), "global" | "captures" | "annotations") {
509 return Err(crate::SigmfError::InvalidMetadata(format!(
510 "cannot insert known field {key:?} as an extension"
511 )));
512 }
513 self.original_bytes = None;
514 Ok(self.extra_fields.insert(key, value))
515 }
516
517 pub fn insert_global_extra(
529 &mut self,
530 key: impl Into<String>,
531 value: Value,
532 ) -> Result<Option<Value>, crate::SigmfError> {
533 self.original_bytes = None;
534 self.global.insert_extra(key, value)
535 }
536
537 pub(crate) fn crop_samples(
538 &self,
539 sample_start: Integer,
540 sample_count: Integer,
541 mut extension_handler: Option<&mut dyn crate::extensions::CropExtensionHandler>,
542 ) -> Result<Self, crate::SigmfError> {
543 let crop_end = sample_start
544 .checked_add(sample_count)
545 .ok_or_else(|| crate::SigmfError::InvalidRange("sample range end overflows".into()))?;
546 if sample_start < self.global.offset.unwrap_or(0) {
547 return Err(crate::SigmfError::InvalidRange(format!(
548 "crop start {sample_start} precedes dataset offset {}",
549 self.global.offset.unwrap_or(0)
550 )));
551 }
552 if !self.extra_fields.is_empty() {
553 return Err(crate::SigmfError::InvalidMetadata(
554 "sample crop cannot safely transform top-level extension fields".into(),
555 ));
556 }
557
558 let mut cropped = self.clone();
559 transform_crop_extension_fields(
560 &mut extension_handler,
561 crate::extensions::ExtensionScope::Global,
562 None,
563 None,
564 sample_start,
565 sample_count,
566 cropped.global.extension_fields_mut(),
567 )?;
568
569 let previous_capture = if sample_count == 0 {
570 None
571 } else {
572 self.captures
573 .iter()
574 .enumerate()
575 .filter(|(_, capture)| capture.sample_start <= sample_start)
576 .max_by_key(|(_, capture)| capture.sample_start)
577 .map(|(index, _)| index)
578 };
579 cropped.captures = self
580 .captures
581 .iter()
582 .enumerate()
583 .filter(|(index, capture)| {
584 sample_count > 0
585 && (Some(*index) == previous_capture
586 || (capture.sample_start >= sample_start
587 && capture.sample_start < crop_end))
588 })
589 .map(|(_, capture)| {
590 let mut capture = capture.clone();
591 transform_crop_extension_fields(
592 &mut extension_handler,
593 crate::extensions::ExtensionScope::Capture,
594 Some(capture.sample_start),
595 None,
596 sample_start,
597 sample_count,
598 capture.extension_fields_mut(),
599 )?;
600 capture.header_bytes = None;
601 Ok(capture)
602 })
603 .collect::<Result<Vec<_>, crate::SigmfError>>()?;
604
605 cropped.annotations = Vec::new();
606 if sample_count > 0 {
607 for annotation in &self.annotations {
608 let (cropped_start, cropped_count) = match annotation.sample_count {
609 Some(count) if count > 0 => {
610 let annotation_end =
611 annotation.sample_start.checked_add(count).ok_or_else(|| {
612 crate::SigmfError::InvalidRange(format!(
613 "annotation at sample {} has an overflowing range",
614 annotation.sample_start
615 ))
616 })?;
617 let start = annotation.sample_start.max(sample_start);
618 let end = annotation_end.min(crop_end);
619 if start >= end {
620 continue;
621 }
622 (start, Some(end - start))
623 }
624 Some(_) => continue,
625 None if annotation.sample_start >= sample_start
626 && annotation.sample_start < crop_end =>
627 {
628 (annotation.sample_start, None)
629 }
630 None => continue,
631 };
632 let mut annotation = annotation.clone();
633 transform_crop_extension_fields(
634 &mut extension_handler,
635 crate::extensions::ExtensionScope::Annotation,
636 Some(annotation.sample_start),
637 annotation.sample_count,
638 sample_start,
639 sample_count,
640 annotation.extension_fields_mut(),
641 )?;
642 annotation.sample_start = cropped_start;
643 annotation.sample_count = cropped_count;
644 cropped.annotations.push(annotation);
645 }
646 }
647
648 cropped.global.offset = Some(sample_start);
649 cropped.global.sha512 = None;
650 cropped.global.dataset = None;
651 cropped.global.trailing_bytes = None;
652 cropped.original_bytes = None;
653 Ok(cropped)
654 }
655
656 pub(crate) fn update_global_extension_fields(
657 &mut self,
658 known_fields: &[&str],
659 fields: Map<String, Value>,
660 ) {
661 self.global.update_extension_fields(known_fields, fields);
662 self.original_bytes = None;
663 }
664
665 pub fn edit(&mut self) -> MetadataEdit<'_> {
671 let draft = self.clone();
672 MetadataEdit {
673 target: self,
674 draft,
675 validation: None,
676 }
677 }
678
679 pub fn set_version(&mut self, version: String) {
686 self.original_bytes = None;
687 self.global.set_version(version);
688 }
689
690 pub fn set_datatype(&mut self, datatype: global::Datatype) {
694 self.original_bytes = None;
695 self.global.set_datatype(datatype);
696 }
697
698 pub fn set_sample_rate(&mut self, sample_rate: Number) {
703 self.original_bytes = None;
704 self.global.set_sample_rate(sample_rate);
705 }
706
707 pub fn set_author(&mut self, author: String) {
711 self.original_bytes = None;
712 self.global.set_author(author);
713 }
714
715 pub fn set_collection(&mut self, collection_str: String) {
720 self.original_bytes = None;
721 self.global.set_collection(collection_str);
722 }
723
724 pub fn set_data_doi(&mut self, data_doi: String) {
728 self.original_bytes = None;
729 self.global.set_data_doi(data_doi);
730 }
731
732 pub fn set_description(&mut self, description: String) {
737 self.original_bytes = None;
738 self.global.set_description(description);
739 }
740
741 pub fn set_hw(&mut self, hw: String) {
746 self.original_bytes = None;
747 self.global.set_hw(hw);
748 }
749
750 pub fn set_license(&mut self, license: String) {
755 self.original_bytes = None;
756 self.global.set_license(license);
757 }
758
759 pub fn set_metadata_only(&mut self, metadata_only: bool) {
764 self.original_bytes = None;
765 self.global.set_metadata_only(metadata_only);
766 }
767
768 pub fn set_meta_doi(&mut self, meta_doi: String) {
772 self.original_bytes = None;
773 self.global.set_meta_doi(meta_doi);
774 }
775
776 pub fn set_num_channels(&mut self, num_channels: Integer) {
781 self.original_bytes = None;
782 self.global.set_num_channels(num_channels);
783 }
784
785 pub fn set_offset(&mut self, offset: Integer) {
790 self.original_bytes = None;
791 self.global.set_offset(offset);
792 }
793
794 pub fn set_recorder(&mut self, recorder: String) {
799 self.original_bytes = None;
800 self.global.set_recorder(recorder);
801 }
802
803 pub fn set_sha512(&mut self, hash: String) {
808 self.original_bytes = None;
809 self.global.set_sha512(hash);
810 }
811
812 pub fn set_geolocation(&mut self, geolocation: GeoJson) {
817 self.original_bytes = None;
818 self.global.set_geolocation(geolocation);
819 }
820
821 pub fn set_extensions(&mut self, extensions: Vec<global::ExtensionDeclaration>) {
826 self.original_bytes = None;
827 self.global.set_extensions(extensions);
828 }
829
830 pub fn set_dataset(&mut self, dataset: String) {
835 self.original_bytes = None;
836 self.global.set_dataset(dataset);
837 }
838
839 pub fn set_trailing_bytes(&mut self, trailing_bytes: Integer) {
844 self.original_bytes = None;
845 self.global.set_trailing_bytes(trailing_bytes);
846 }
847
848 pub fn add_annotation(&mut self, annotation: annotation::Annotation) {
854 self.original_bytes = None;
855 self.annotations.push(annotation);
856 }
857
858 pub fn add_capture(&mut self, capture: capture::Capture) {
864 self.original_bytes = None;
865 self.captures.push(capture);
866 }
867
868 fn validate_metadata_shape(value: &Value) -> Result<(), crate::SigmfError> {
869 let object = value.as_object().ok_or_else(|| {
870 crate::SigmfError::InvalidMetadata("expected an object at JSON Pointer /".to_string())
871 })?;
872 let global = object
873 .get("global")
874 .and_then(Value::as_object)
875 .ok_or_else(|| {
876 crate::SigmfError::InvalidMetadata(
877 "expected an object at JSON Pointer /global".to_string(),
878 )
879 })?;
880 for required in ["core:version", "core:datatype"] {
881 if !global.contains_key(required) {
882 return Err(crate::SigmfError::InvalidMetadata(format!(
883 "missing required field at JSON Pointer /global/{required}"
884 )));
885 }
886 }
887 if !global["core:version"].is_string() {
888 return Err(crate::SigmfError::InvalidMetadata(
889 "expected a string at JSON Pointer /global/core:version".to_string(),
890 ));
891 }
892 if !global["core:datatype"].is_string() {
893 return Err(crate::SigmfError::InvalidMetadata(
894 "expected a string at JSON Pointer /global/core:datatype".to_string(),
895 ));
896 }
897 Self::reject_null_fields(
898 global,
899 "/global",
900 &[
901 "core:sample_rate",
902 "core:author",
903 "core:collection",
904 "core:data_doi",
905 "core:description",
906 "core:hw",
907 "core:license",
908 "core:metadata_only",
909 "core:meta_doi",
910 "core:num_channels",
911 "core:offset",
912 "core:recorder",
913 "core:sha512",
914 "core:geolocation",
915 "core:extensions",
916 "core:dataset",
917 "core:trailing_bytes",
918 ],
919 )?;
920 for required in ["captures", "annotations"] {
921 if !object.get(required).is_some_and(Value::is_array) {
922 return Err(crate::SigmfError::InvalidMetadata(format!(
923 "expected an array at JSON Pointer /{required}"
924 )));
925 }
926 }
927 for (name, field) in [
928 ("captures", "core:sample_start"),
929 ("annotations", "core:sample_start"),
930 ] {
931 for (index, item) in object[name].as_array().into_iter().flatten().enumerate() {
932 let pointer = format!("/{name}/{index}");
933 let Some(item) = item.as_object() else {
934 return Err(crate::SigmfError::InvalidMetadata(format!(
935 "expected an object at JSON Pointer {pointer}"
936 )));
937 };
938 if !item.contains_key(field) {
939 return Err(crate::SigmfError::InvalidMetadata(format!(
940 "missing required field at JSON Pointer /{name}/{index}/{field}"
941 )));
942 }
943 if !item[field].is_u64() {
944 return Err(crate::SigmfError::InvalidMetadata(format!(
945 "expected a nonnegative integer at JSON Pointer /{name}/{index}/{field}"
946 )));
947 }
948 let optional_fields = if name == "captures" {
949 &[
950 "core:datetime",
951 "core:frequency",
952 "core:global_index",
953 "core:header_bytes",
954 "core:geolocation",
955 ][..]
956 } else {
957 &[
958 "core:sample_count",
959 "core:freq_lower_edge",
960 "core:freq_upper_edge",
961 "core:label",
962 "core:comment",
963 "core:generator",
964 "core:uuid",
965 ][..]
966 };
967 Self::reject_null_fields(item, &pointer, optional_fields)?;
968 }
969 }
970 Ok(())
971 }
972
973 fn reject_null_fields(
974 object: &serde_json::Map<String, Value>,
975 pointer: &str,
976 fields: &[&str],
977 ) -> Result<(), crate::SigmfError> {
978 for field in fields {
979 if object.get(*field).is_some_and(Value::is_null) {
980 return Err(crate::SigmfError::InvalidMetadata(format!(
981 "null is not permitted at JSON Pointer {pointer}/{field}; omit the field instead"
982 )));
983 }
984 }
985 Ok(())
986 }
987
988 pub fn get_version(&self) -> String {
993 self.global.get_version()
994 }
995
996 pub fn get_datatype(&self) -> global::Datatype {
999 self.global.get_datatype()
1000 }
1001
1002 pub fn get_sample_rate(&self) -> Option<Number> {
1005 self.global.get_sample_rate()
1006 }
1007
1008 pub fn get_author(&self) -> Option<String> {
1011 self.global.get_author()
1012 }
1013
1014 pub fn get_collection(&self) -> Option<String> {
1017 self.global.get_collection()
1018 }
1019
1020 pub fn get_data_doi(&self) -> Option<String> {
1023 self.global.get_data_doi()
1024 }
1025
1026 pub fn get_description(&self) -> Option<String> {
1029 self.global.get_description()
1030 }
1031
1032 pub fn get_hw(&self) -> Option<String> {
1035 self.global.get_hw()
1036 }
1037
1038 pub fn get_license(&self) -> Option<String> {
1041 self.global.get_license()
1042 }
1043
1044 pub fn get_metadata_only(&self) -> Option<bool> {
1047 self.global.get_metadata_only()
1048 }
1049
1050 pub fn get_meta_doi(&self) -> Option<String> {
1053 self.global.get_meta_doi()
1054 }
1055
1056 pub fn get_num_channels(&self) -> Option<Integer> {
1059 self.global.get_num_channels()
1060 }
1061
1062 pub fn get_offset(&self) -> Option<Integer> {
1065 self.global.get_offset()
1066 }
1067
1068 pub fn get_recorder(&self) -> Option<String> {
1071 self.global.get_recorder()
1072 }
1073
1074 pub fn get_sha512(&self) -> Option<String> {
1077 self.global.get_sha512()
1078 }
1079
1080 pub fn get_geolocation(&self) -> Option<GeoJson> {
1083 self.global.get_geolocation()
1084 }
1085
1086 pub fn get_extensions(&self) -> Option<Vec<global::ExtensionDeclaration>> {
1089 self.global.get_extensions()
1090 }
1091
1092 pub fn get_dataset(&self) -> Option<String> {
1095 self.global.get_dataset()
1096 }
1097
1098 pub fn get_trailing_bytes(&self) -> Option<Integer> {
1101 self.global.get_trailing_bytes()
1102 }
1103
1104 pub fn get_annotations(&self) -> Vec<annotation::Annotation> {
1109 self.annotations.clone()
1110 }
1111
1112 pub fn get_captures(&self) -> Vec<capture::Capture> {
1117 self.captures.clone()
1118 }
1119}
1120
1121fn transform_crop_extension_fields(
1122 handler: &mut Option<&mut dyn crate::extensions::CropExtensionHandler>,
1123 scope: crate::extensions::ExtensionScope,
1124 sample_start: Option<Integer>,
1125 sample_count: Option<Integer>,
1126 crop_start: Integer,
1127 crop_count: Integer,
1128 fields: &mut Map<String, Value>,
1129) -> Result<(), crate::SigmfError> {
1130 if fields.is_empty() {
1131 return Ok(());
1132 }
1133 let Some(handler) = handler.as_mut() else {
1134 return Err(crate::SigmfError::InvalidMetadata(format!(
1135 "sample crop requires an extension handler for {scope:?} extension fields"
1136 )));
1137 };
1138 (**handler).transform_for_crop(
1139 scope,
1140 sample_start,
1141 sample_count,
1142 crop_start,
1143 crop_count,
1144 fields,
1145 )
1146}
1147
1148#[derive(Debug, Clone, PartialEq)]
1150pub struct MetadataChange {
1151 pub json_pointer: String,
1154 pub before: Option<Value>,
1156 pub after: Option<Value>,
1158}
1159
1160fn collect_changes(
1161 pointer: &str,
1162 before: Option<&Value>,
1163 after: Option<&Value>,
1164 changes: &mut Vec<MetadataChange>,
1165) {
1166 if before == after {
1167 return;
1168 }
1169 if let (Some(Value::Object(before)), Some(Value::Object(after))) = (before, after) {
1170 let keys = before
1171 .keys()
1172 .chain(after.keys())
1173 .collect::<std::collections::BTreeSet<_>>();
1174 for key in keys {
1175 let escaped = key.replace('~', "~0").replace('/', "~1");
1176 let child_pointer = format!("{pointer}/{escaped}");
1177 collect_changes(&child_pointer, before.get(key), after.get(key), changes);
1178 }
1179 return;
1180 }
1181 if let (Some(Value::Array(before)), Some(Value::Array(after))) = (before, after) {
1182 for index in 0..before.len().max(after.len()) {
1183 let child_pointer = format!("{pointer}/{index}");
1184 collect_changes(&child_pointer, before.get(index), after.get(index), changes);
1185 }
1186 return;
1187 }
1188 changes.push(MetadataChange {
1189 json_pointer: if pointer.is_empty() {
1190 "/".to_string()
1191 } else {
1192 pointer.to_string()
1193 },
1194 before: before.cloned(),
1195 after: after.cloned(),
1196 });
1197}
1198
1199pub struct MetadataEdit<'a> {
1231 target: &'a mut Metadata,
1232 draft: Metadata,
1233 validation: Option<crate::validation::ValidationReport>,
1234}
1235
1236impl MetadataEdit<'_> {
1237 pub fn metadata(&self) -> &Metadata {
1239 &self.draft
1240 }
1241
1242 pub fn metadata_mut(&mut self) -> &mut Metadata {
1248 self.validation = None;
1249 self.draft.original_bytes = None;
1250 &mut self.draft
1251 }
1252
1253 pub fn set_description(&mut self, description: String) {
1256 self.draft.set_description(description);
1257 self.validation = None;
1258 }
1259
1260 pub fn set_datatype(&mut self, datatype: global::Datatype) {
1263 self.draft.set_datatype(datatype);
1264 self.validation = None;
1265 }
1266
1267 pub fn set_offset(&mut self, offset: Integer) {
1270 self.draft.set_offset(offset);
1271 self.validation = None;
1272 }
1273
1274 pub fn add_annotation(&mut self, annotation: annotation::Annotation) {
1276 self.draft.add_annotation(annotation);
1277 self.validation = None;
1278 }
1279
1280 pub fn replace_annotation(
1285 &mut self,
1286 index: usize,
1287 annotation: annotation::Annotation,
1288 ) -> Option<annotation::Annotation> {
1289 let current = self.draft.annotations.get_mut(index)?;
1290 self.validation = None;
1291 Some(std::mem::replace(current, annotation))
1292 }
1293
1294 pub fn add_capture(&mut self, capture: capture::Capture) {
1296 self.draft.add_capture(capture);
1297 self.validation = None;
1298 }
1299
1300 pub fn replace_capture(
1305 &mut self,
1306 index: usize,
1307 capture: capture::Capture,
1308 ) -> Option<capture::Capture> {
1309 let current = self.draft.captures.get_mut(index)?;
1310 self.validation = None;
1311 Some(std::mem::replace(current, capture))
1312 }
1313
1314 pub fn remove_annotation(&mut self, index: usize) -> Option<annotation::Annotation> {
1320 if index < self.draft.annotations.len() {
1321 self.validation = None;
1322 Some(self.draft.annotations.remove(index))
1323 } else {
1324 None
1325 }
1326 }
1327
1328 pub fn remove_capture(&mut self, index: usize) -> Option<capture::Capture> {
1334 if index < self.draft.captures.len() {
1335 self.validation = None;
1336 Some(self.draft.captures.remove(index))
1337 } else {
1338 None
1339 }
1340 }
1341
1342 pub fn stable_sort_segments(&mut self) {
1348 self.draft
1349 .captures
1350 .sort_by_key(|capture| capture.sample_start);
1351 self.draft
1352 .annotations
1353 .sort_by_key(|annotation| annotation.sample_start);
1354 self.validation = None;
1355 }
1356
1357 pub fn validate(
1364 &mut self,
1365 policy: crate::validation::ValidationPolicy,
1366 ) -> crate::validation::ValidationReport {
1367 let revision = self.draft.revision_fingerprint();
1368 let mut report = self.draft.validate(policy);
1369 report.revision = Some(revision);
1370 report.policy = Some(policy);
1371 self.validation = Some(report.clone());
1372 report
1373 }
1374
1375 pub fn commit(self) -> Result<(), crate::SigmfError> {
1388 let report = self.validation.ok_or_else(|| {
1389 crate::SigmfError::InvalidMetadata(
1390 "metadata edits require a fresh validation report before commit".to_string(),
1391 )
1392 })?;
1393 if report.status == crate::validation::ValidationStatus::Failed {
1394 return Err(crate::SigmfError::InvalidMetadata(
1395 "metadata edit failed validation".to_string(),
1396 ));
1397 }
1398 let revision = self.draft.revision_fingerprint();
1399 if report.revision.as_deref() != Some(revision.as_str()) || report.policy.is_none() {
1400 return Err(crate::SigmfError::InvalidMetadata(
1401 "validation report is stale or belongs to a different context".to_string(),
1402 ));
1403 }
1404 *self.target = self.draft;
1405 Ok(())
1406 }
1407}
1408
1409#[cfg(test)]
1410mod tests {
1411 use super::*;
1412
1413 const METADATA: &[u8] = br#"{
1414 "global": {
1415 "core:version": "1.2.6",
1416 "core:datatype": "ri16_le",
1417 "vendor:global": {"enabled": true}
1418 },
1419 "captures": [{"core:sample_start": 0, "core:frequency": 915000000}],
1420 "annotations": [{"core:sample_start": 2, "vendor:label": "burst"}],
1421 "vendor:top": 9007199254740993
1422 }"#;
1423
1424 #[test]
1425 fn metadata_round_trip_preserves_unknown_fields_and_omits_absent_options() {
1426 let metadata = Metadata::from_slice(METADATA).unwrap();
1427 let output = metadata.to_vec().unwrap();
1428 let value: Value = serde_json::from_slice(&output).unwrap();
1429
1430 assert_eq!(value["global"]["vendor:global"]["enabled"], true);
1431 assert_eq!(value["captures"][0]["core:frequency"], 915000000.0);
1432 assert_eq!(value["annotations"][0]["vendor:label"], "burst");
1433 assert_eq!(value["vendor:top"], 9007199254740993u64);
1434 assert!(value["global"].get("core:author").is_none());
1435 assert!(value["annotations"][0].get("core:sample_count").is_none());
1436 assert_eq!(metadata.original_bytes(), Some(METADATA));
1437 }
1438
1439 #[test]
1440 fn original_bytes_are_invalidated_by_metadata_edits() {
1441 let mut metadata = Metadata::from_slice(METADATA).unwrap();
1442 assert!(metadata.original_digest().unwrap().is_some());
1443 metadata.set_description("edited".to_string());
1444 assert!(metadata.original_bytes().is_none());
1445 assert!(metadata.original_digest().unwrap().is_none());
1446 }
1447
1448 #[test]
1449 fn required_fields_report_json_pointers() {
1450 let error = Metadata::from_slice(
1451 br#"{"global":{"core:datatype":"ri16_le"},"captures":[],"annotations":[]}"#,
1452 )
1453 .unwrap_err()
1454 .to_string();
1455 assert!(error.contains("/global/core:version"), "{error}");
1456
1457 let error = Metadata::from_slice(
1458 br#"{"global":{"core:version":"1.2.6","core:datatype":"ri16_le"}}"#,
1459 )
1460 .unwrap_err()
1461 .to_string();
1462 assert!(error.contains("/captures"), "{error}");
1463 }
1464
1465 #[test]
1466 fn slice_parser_enforces_metadata_size_limit() {
1467 let oversized = vec![b' '; crate::MAX_META_SIZE + 1];
1468 assert!(matches!(
1469 Metadata::from_slice(&oversized),
1470 Err(crate::SigmfError::ResourceLimit(_))
1471 ));
1472 }
1473
1474 #[test]
1475 fn known_keys_cannot_be_inserted_as_extensions() {
1476 let mut metadata = Metadata::from_slice(METADATA).unwrap();
1477 assert!(
1478 metadata
1479 .insert_global_extra("core:version", Value::String("2".into()))
1480 .is_err()
1481 );
1482 assert!(
1483 metadata
1484 .insert_extra("captures", Value::Array(Vec::new()))
1485 .is_err()
1486 );
1487 assert!(
1488 metadata
1489 .insert_extra("vendor:other", Value::Bool(true))
1490 .is_ok()
1491 );
1492 }
1493
1494 #[test]
1495 fn queries_use_absolute_half_open_ranges_and_explicit_frequency_bounds() {
1496 let metadata = Metadata::from_slice(
1497 br#"{
1498 "global": {"core:version":"1.2.6","core:datatype":"ri16_le"},
1499 "captures":[{"core:sample_start":10},{"core:sample_start":20}],
1500 "annotations":[
1501 {"core:sample_start":2,"core:sample_count":4,"core:freq_lower_edge":100,"core:freq_upper_edge":200},
1502 {"core:sample_start":6,"core:sample_count":0,"core:freq_lower_edge":300}
1503 ]
1504 }"#,
1505 )
1506 .unwrap();
1507 assert_eq!(metadata.capture_at(19).unwrap().sample_start, 10);
1508 assert_eq!(metadata.capture_at(20).unwrap().sample_start, 20);
1509 assert!(metadata.capture_at(9).is_none());
1510 assert_eq!(
1511 metadata
1512 .annotations_intersecting_samples(5, 2)
1513 .unwrap()
1514 .len(),
1515 1
1516 );
1517 assert!(
1518 metadata
1519 .annotations_intersecting_frequency(200.0, 250.0)
1520 .unwrap()
1521 .len()
1522 == 1
1523 );
1524 assert!(
1525 metadata
1526 .annotations_intersecting_samples(u64::MAX, 1)
1527 .is_err()
1528 );
1529 assert!(
1530 metadata
1531 .annotations_intersecting_frequency(2.0, 1.0)
1532 .is_err()
1533 );
1534 }
1535
1536 #[test]
1537 fn metadata_diff_uses_escaped_json_pointers() {
1538 let before = Metadata::from_slice(METADATA).unwrap();
1539 let mut after = before.clone();
1540 after
1541 .insert_extra("vendor/a~b", Value::String("changed".into()))
1542 .unwrap();
1543 let changes = before.diff(&after).unwrap();
1544 assert_eq!(changes.len(), 1);
1545 assert_eq!(changes[0].json_pointer, "/vendor~1a~0b");
1546 }
1547
1548 #[test]
1549 fn edit_commit_is_transactional_and_requires_current_validation() {
1550 let mut metadata = Metadata::from_slice(METADATA).unwrap();
1551 {
1552 let mut edit = metadata.edit();
1553 edit.set_description("reviewed".into());
1554 assert_eq!(
1555 edit.validate(crate::validation::ValidationPolicy::StrictSupportedVersion)
1556 .status,
1557 crate::validation::ValidationStatus::NotChecked
1558 );
1559 edit.commit().unwrap();
1560 }
1561 assert_eq!(metadata.get_description().as_deref(), Some("reviewed"));
1562
1563 let before = metadata.to_vec().unwrap();
1564 let mut edit = metadata.edit();
1565 edit.add_annotation(annotation::Annotation::new(
1566 4,
1567 None,
1568 Some(1.0),
1569 None,
1570 None,
1571 None,
1572 None,
1573 None,
1574 ));
1575 let report = edit.validate(crate::validation::ValidationPolicy::StrictSupportedVersion);
1576 assert_eq!(report.status, crate::validation::ValidationStatus::Failed);
1577 assert!(edit.commit().is_err());
1578 assert_eq!(metadata.to_vec().unwrap(), before);
1579
1580 let mut edit = metadata.edit();
1581 let _ = edit.validate(crate::validation::ValidationPolicy::StrictSupportedVersion);
1582 edit.set_description("stale".into());
1583 assert!(edit.commit().is_err());
1584 assert_eq!(metadata.get_description().as_deref(), Some("reviewed"));
1585 }
1586
1587 #[test]
1588 fn edit_stable_sort_normalizes_segments_transactionally() {
1589 let mut metadata = Metadata::from_slice(METADATA).unwrap();
1590 metadata.add_capture(capture::Capture::new(8));
1591 metadata.add_capture(capture::Capture::new(3));
1592 metadata.add_annotation(annotation::Annotation::new(
1593 9, None, None, None, None, None, None, None,
1594 ));
1595 metadata.add_annotation(annotation::Annotation::new(
1596 1, None, None, None, None, None, None, None,
1597 ));
1598
1599 let mut edit = metadata.edit();
1600 edit.stable_sort_segments();
1601 assert_eq!(
1602 edit.validate(crate::validation::ValidationPolicy::StrictSupportedVersion)
1603 .status,
1604 crate::validation::ValidationStatus::NotChecked
1605 );
1606 edit.commit().unwrap();
1607 assert_eq!(
1608 metadata
1609 .captures()
1610 .iter()
1611 .map(|capture| capture.sample_start)
1612 .collect::<Vec<_>>(),
1613 [0, 3, 8]
1614 );
1615 assert_eq!(
1616 metadata
1617 .annotations()
1618 .iter()
1619 .map(|annotation| annotation.sample_start)
1620 .collect::<Vec<_>>(),
1621 [1, 2, 9]
1622 );
1623 }
1624
1625 #[test]
1626 fn metadata_save_returns_digest_of_exact_bytes_and_does_not_overwrite() {
1627 let directory = std::env::temp_dir().join(format!(
1628 "sigmf-metadata-save-test-{}-{}",
1629 std::process::id(),
1630 std::time::SystemTime::now()
1631 .duration_since(std::time::UNIX_EPOCH)
1632 .unwrap()
1633 .as_nanos()
1634 ));
1635 std::fs::create_dir_all(&directory).unwrap();
1636 let path = directory.join("capture.sigmf-meta");
1637 let metadata = Metadata::from_slice(METADATA).unwrap();
1638 let digest = metadata
1639 .save_new(
1640 &path,
1641 crate::validation::ValidationPolicy::StrictSupportedVersion,
1642 )
1643 .unwrap();
1644 let written = std::fs::read(&path).unwrap();
1645 assert_eq!(digest, hash_from_slice(&written).unwrap());
1646 assert!(
1647 metadata
1648 .save_new(
1649 &path,
1650 crate::validation::ValidationPolicy::StrictSupportedVersion,
1651 )
1652 .is_err()
1653 );
1654 assert_eq!(std::fs::read(&path).unwrap(), written);
1655 std::fs::remove_dir_all(directory).unwrap();
1656 }
1657}