Skip to main content

sigmf/
meta.rs

1//! SigMF recording metadata: the contents of a `.sigmf-meta` file.
2//!
3//! A SigMF metadata document is a JSON object with three required top-level
4//! keys, each modeled by its own type:
5//!
6//! * `global` — recording-wide properties such as `core:datatype`,
7//!   `core:sample_rate`, and `core:version` ([`crate::global::Global`]);
8//! * `captures` — segments of the dataset keyed by `core:sample_start`
9//!   ([`crate::capture::Capture`]);
10//! * `annotations` — labeled sample and frequency ranges
11//!   ([`crate::annotation::Annotation`]).
12//!
13//! [`Metadata`] ties these together, parses and serializes the document
14//! (preserving unknown extension keys), offers range queries over captures
15//! and annotations, and writes validated metadata to disk. [`MetadataEdit`]
16//! provides transactional, validate-then-commit editing, and
17//! [`MetadataChange`] describes differences reported by [`Metadata::diff`].
18
19use 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/// A complete SigMF metadata document (`.sigmf-meta`).
27///
28/// Holds the `global` object, the `captures` and `annotations` arrays, and any
29/// unrecognized top-level keys. When parsed with [`Metadata::from_slice`] (or
30/// [`from_reader`](Metadata::from_reader) / [`from_path`](Metadata::from_path))
31/// the exact input bytes are retained and exposed through
32/// [`Metadata::original_bytes`]; any mutation through this type's methods
33/// discards them.
34///
35/// # Examples
36///
37/// ```rust
38/// use sigmf::capture::Capture;
39/// use sigmf::global::{Datatype, Global};
40/// use sigmf::meta::Metadata;
41///
42/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
43/// let global = Global::new("1.2.6", "cf32_le".parse::<Datatype>()?);
44/// let mut metadata = Metadata::new(global, Vec::new(), vec![Capture::new(0)]);
45/// metadata.set_sample_rate(1_000_000.0);
46///
47/// let bytes = metadata.to_vec()?;
48/// let parsed = Metadata::from_slice(&bytes)?;
49/// assert_eq!(parsed.get_sample_rate(), Some(1_000_000.0));
50/// assert_eq!(parsed.captures().len(), 1);
51/// # Ok(())
52/// # }
53/// ```
54#[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    /// Creates a metadata document from its `global` object and its
67    /// `annotations` and `captures` arrays.
68    ///
69    /// The document has no top-level extension fields and no retained
70    /// original bytes. No validation is performed; see
71    /// [`Metadata::validate`].
72    ///
73    /// # Examples
74    ///
75    /// ```rust
76    /// use sigmf::capture::Capture;
77    /// use sigmf::global::{Datatype, Global};
78    /// use sigmf::meta::Metadata;
79    ///
80    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
81    /// let global = Global::new("1.2.6", "ri16_le".parse::<Datatype>()?);
82    /// let metadata = Metadata::new(global, Vec::new(), vec![Capture::new(0)]);
83    /// assert_eq!(metadata.get_version(), "1.2.6");
84    /// assert!(metadata.annotations().is_empty());
85    /// # Ok(())
86    /// # }
87    /// ```
88    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    // ================================== Methods ================================== //
102
103    /// Parses a metadata document from JSON bytes.
104    ///
105    /// Before deserializing, the input is checked for duplicate object keys
106    /// and for the basic shape required by SigMF: a `global` object with
107    /// string `core:version` and `core:datatype`, `captures` and
108    /// `annotations` arrays whose entries are objects with a non-negative
109    /// integer `core:sample_start`, and no explicit `null` for optional core
110    /// fields. Unknown keys are preserved. The input bytes are retained and
111    /// available via [`Metadata::original_bytes`].
112    ///
113    /// This does not run full semantic validation; see
114    /// [`Metadata::validate`].
115    ///
116    /// # Errors
117    ///
118    /// * [`crate::SigmfError::ResourceLimit`] if `bytes` exceeds the 1 MB
119    ///   metadata size limit.
120    /// * [`crate::SigmfError::InvalidJson`] if the input is not valid JSON or
121    ///   an object contains a duplicate key.
122    /// * [`crate::SigmfError::InvalidMetadata`] if a required field is
123    ///   missing, has the wrong type, an optional core field is `null`, or the
124    ///   document otherwise fails to deserialize (including an invalid
125    ///   `core:datatype`).
126    ///
127    /// # Examples
128    ///
129    /// ```rust
130    /// use sigmf::meta::Metadata;
131    ///
132    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
133    /// let json = br#"{
134    ///     "global": {"core:version": "1.2.6", "core:datatype": "cf32_le"},
135    ///     "captures": [{"core:sample_start": 0, "core:frequency": 915e6}],
136    ///     "annotations": []
137    /// }"#;
138    /// let metadata = Metadata::from_slice(json)?;
139    /// assert_eq!(metadata.get_datatype().to_string(), "cf32_le");
140    /// assert_eq!(metadata.captures()[0].frequency, Some(915e6));
141    /// assert_eq!(metadata.original_bytes(), Some(&json[..]));
142    ///
143    /// assert!(Metadata::from_slice(br#"{"global": {}}"#).is_err());
144    /// # Ok(())
145    /// # }
146    /// ```
147    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    /// Serializes the document as compact JSON.
163    ///
164    /// Optional fields that are unset are omitted, and extension fields are
165    /// written back alongside the core fields. The output is freshly
166    /// serialized and is not guaranteed to match [`Metadata::original_bytes`].
167    ///
168    /// # Errors
169    ///
170    /// Returns [`crate::SigmfError::Serde`] if serialization fails.
171    pub fn to_vec(&self) -> Result<Vec<u8>, crate::SigmfError> {
172        Ok(serde_json::to_vec(self)?)
173    }
174
175    /// Serializes the document as pretty-printed (indented) JSON.
176    ///
177    /// # Errors
178    ///
179    /// Returns [`crate::SigmfError::Serde`] if serialization fails.
180    pub fn to_vec_pretty(&self) -> Result<Vec<u8>, crate::SigmfError> {
181        Ok(serde_json::to_vec_pretty(self)?)
182    }
183
184    /// Validates the document under `policy`, then writes its compact JSON
185    /// serialization to `writer`.
186    ///
187    /// Returns the SHA-512 digest (lowercase hex) of the exact bytes written,
188    /// suitable for a collection's `core:streams` `hash` entry. Validation
189    /// warnings do not prevent writing.
190    ///
191    /// # Errors
192    ///
193    /// * [`crate::SigmfError::InvalidMetadata`] if validation fails; nothing
194    ///   is written in that case.
195    /// * [`crate::SigmfError::Serde`] if serialization fails.
196    /// * [`crate::SigmfError::Io`] if writing to `writer` fails.
197    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    /// Validates the document under `policy` and atomically writes it to a new
214    /// file at `path`.
215    ///
216    /// The JSON is written to a temporary sibling file, synced, and then
217    /// published under `path` without overwriting an existing file. Returns
218    /// the SHA-512 digest (lowercase hex) of the bytes written.
219    ///
220    /// # Errors
221    ///
222    /// * [`crate::SigmfError::InvalidMetadata`] if validation fails or `path`
223    ///   does not name a file.
224    /// * [`crate::SigmfError::Serde`] if serialization fails.
225    /// * [`crate::SigmfError::Io`] if `path` already exists or any file
226    ///   system operation fails.
227    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    /// Validates the document under `policy` and atomically writes it to
236    /// `path`, replacing any existing file.
237    ///
238    /// The JSON is written to a temporary sibling file, synced, and renamed
239    /// over `path`, so readers observe either the old or the new contents.
240    /// Returns the SHA-512 digest (lowercase hex) of the bytes written.
241    ///
242    /// # Errors
243    ///
244    /// * [`crate::SigmfError::InvalidMetadata`] if validation fails or `path`
245    ///   does not name a file.
246    /// * [`crate::SigmfError::Serde`] if serialization fails.
247    /// * [`crate::SigmfError::Io`] if any file system operation fails.
248    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    /// Reads and parses a metadata document from `reader`.
275    ///
276    /// At most 1 MB plus one byte is read; the bytes are then parsed with
277    /// [`Metadata::from_slice`].
278    ///
279    /// # Errors
280    ///
281    /// * [`crate::SigmfError::Io`] if reading fails.
282    /// * [`crate::SigmfError::InvalidMetadata`] if the input exceeds the 1 MB
283    ///   metadata size limit.
284    /// * Any error returned by [`Metadata::from_slice`].
285    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    /// Opens the `.sigmf-meta` file at `path` and parses it with
300    /// [`Metadata::from_reader`].
301    ///
302    /// # Errors
303    ///
304    /// Returns [`crate::SigmfError::Io`] if the file cannot be opened, or any
305    /// error returned by [`Metadata::from_reader`].
306    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    /// Returns the exact bytes this document was parsed from, if it was
311    /// parsed and has not been modified since.
312    ///
313    /// Returns `None` for documents built with [`Metadata::new`] and after
314    /// calling a mutating method on [`Metadata`] (setters, `insert_*`, or
315    /// `add_*`).
316    pub fn original_bytes(&self) -> Option<&[u8]> {
317        self.original_bytes.as_deref()
318    }
319
320    /// Returns the SHA-512 digest (lowercase hex) of
321    /// [`Metadata::original_bytes`], or `None` if no original bytes are
322    /// retained.
323    ///
324    /// This is the hash a collection's `core:streams` entry should record for
325    /// an unmodified, previously parsed metadata file.
326    ///
327    /// # Errors
328    ///
329    /// Returns a [`crate::SigmfError`] if hashing fails (currently
330    /// infallible).
331    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    /// Returns the SHA-512 digest (lowercase hex) of the document's compact
339    /// serialization, as produced by [`Metadata::to_vec`].
340    ///
341    /// This may differ from [`Metadata::original_digest`] even for an
342    /// unmodified document, because re-serialization does not preserve the
343    /// original formatting.
344    ///
345    /// # Errors
346    ///
347    /// Returns [`crate::SigmfError::Serde`] if serialization fails.
348    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    /// Returns the `global` object.
359    pub fn global(&self) -> &global::Global {
360        &self.global
361    }
362
363    /// Returns the `captures` array in document order.
364    pub fn captures(&self) -> &[capture::Capture] {
365        &self.captures
366    }
367
368    /// Returns the `annotations` array in document order.
369    pub fn annotations(&self) -> &[annotation::Annotation] {
370        &self.annotations
371    }
372
373    /// Returns the unrecognized top-level keys (anything other than `global`,
374    /// `captures`, and `annotations`).
375    pub fn extra_fields(&self) -> &Map<String, Value> {
376        &self.extra_fields
377    }
378
379    /// Returns the capture segment that contains `sample_index`.
380    ///
381    /// This is the capture with the greatest `core:sample_start` that is less
382    /// than or equal to `sample_index`, regardless of the order of the
383    /// `captures` array. Returns `None` if every capture starts after
384    /// `sample_index` or there are no captures. If several captures share
385    /// that start, the last one in document order is returned.
386    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    /// Returns the annotations whose sample range overlaps the half-open
394    /// range `[start, start + sample_count)`.
395    ///
396    /// An annotation's range is `[core:sample_start, core:sample_start +
397    /// core:sample_count)`. Annotations without `core:sample_count`, or with
398    /// a count of zero, never match. An empty query range returns no
399    /// annotations. Results are in document order.
400    ///
401    /// # Errors
402    ///
403    /// Returns [`crate::SigmfError::InvalidRange`] if `start + sample_count`
404    /// overflows, or if a candidate annotation's end overflows.
405    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    /// Returns the annotations whose frequency range overlaps the closed
438    /// interval `[lower, upper]` (in Hz).
439    ///
440    /// Only annotations that set both `core:freq_lower_edge` and
441    /// `core:freq_upper_edge` are considered; touching edges count as an
442    /// overlap. Results are in document order.
443    ///
444    /// # Errors
445    ///
446    /// Returns [`crate::SigmfError::InvalidRange`] if either bound is not
447    /// finite or if `lower > upper`.
448    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    /// Computes the leaf-level differences between this document and
472    /// `other`.
473    ///
474    /// Both documents are serialized to JSON and compared recursively.
475    /// Objects are compared key by key and arrays index by index, so
476    /// inserting an element shifts every later index. Each reported change
477    /// carries an RFC 6901 JSON Pointer; the result is empty when the
478    /// serializations are equal.
479    ///
480    /// # Errors
481    ///
482    /// Returns [`crate::SigmfError::Serde`] if either document fails to
483    /// serialize.
484    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    /// Inserts or replaces a top-level extension key.
493    ///
494    /// Returns the previous value under `key`, if any, and clears the
495    /// retained [original bytes](Metadata::original_bytes). The key is not
496    /// checked against declared extensions.
497    ///
498    /// # Errors
499    ///
500    /// Returns [`crate::SigmfError::InvalidMetadata`] if `key` is `global`,
501    /// `captures`, or `annotations`.
502    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    /// Inserts or replaces an extension key in the `global` object.
518    ///
519    /// Returns the previous value under `key`, if any. Delegates to
520    /// [`crate::global::Global::insert_extra`]. The retained
521    /// [original bytes](Metadata::original_bytes) are cleared even if the
522    /// insertion is rejected.
523    ///
524    /// # Errors
525    ///
526    /// Returns [`crate::SigmfError::InvalidMetadata`] if `key` names a core
527    /// global field (for example `core:version`).
528    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    /// Starts a transactional edit of this document.
666    ///
667    /// Changes are made to a draft copy; they are applied to `self` only when
668    /// [`MetadataEdit::commit`] succeeds, and discarded if the
669    /// [`MetadataEdit`] is dropped.
670    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    // ================================== Setters ================================== //
680    // ---------------- Global ---------------- //
681    /// Sets the global `core:version` field: the SigMF specification version
682    /// string.
683    ///
684    /// Clears the retained [original bytes](Metadata::original_bytes).
685    pub fn set_version(&mut self, version: String) {
686        self.original_bytes = None;
687        self.global.set_version(version);
688    }
689
690    /// Sets the global `core:datatype` field: the sample format of the dataset.
691    ///
692    /// Clears the retained [original bytes](Metadata::original_bytes).
693    pub fn set_datatype(&mut self, datatype: global::Datatype) {
694        self.original_bytes = None;
695        self.global.set_datatype(datatype);
696    }
697
698    /// Sets the global `core:sample_rate` field: the sample rate in samples per
699    /// second.
700    ///
701    /// Clears the retained [original bytes](Metadata::original_bytes).
702    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    /// Sets the global `core:author` field: the author of the recording.
708    ///
709    /// Clears the retained [original bytes](Metadata::original_bytes).
710    pub fn set_author(&mut self, author: String) {
711        self.original_bytes = None;
712        self.global.set_author(author);
713    }
714
715    /// Sets the global `core:collection` field: the base name of the SigMF
716    /// collection this recording belongs to.
717    ///
718    /// Clears the retained [original bytes](Metadata::original_bytes).
719    pub fn set_collection(&mut self, collection_str: String) {
720        self.original_bytes = None;
721        self.global.set_collection(collection_str);
722    }
723
724    /// Sets the global `core:data_doi` field: the DOI of the dataset file.
725    ///
726    /// Clears the retained [original bytes](Metadata::original_bytes).
727    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    /// Sets the global `core:description` field: the free-text description of
733    /// the recording.
734    ///
735    /// Clears the retained [original bytes](Metadata::original_bytes).
736    pub fn set_description(&mut self, description: String) {
737        self.original_bytes = None;
738        self.global.set_description(description);
739    }
740
741    /// Sets the global `core:hw` field: the description of the recording
742    /// hardware.
743    ///
744    /// Clears the retained [original bytes](Metadata::original_bytes).
745    pub fn set_hw(&mut self, hw: String) {
746        self.original_bytes = None;
747        self.global.set_hw(hw);
748    }
749
750    /// Sets the global `core:license` field: the URL of the license governing
751    /// the recording.
752    ///
753    /// Clears the retained [original bytes](Metadata::original_bytes).
754    pub fn set_license(&mut self, license: String) {
755        self.original_bytes = None;
756        self.global.set_license(license);
757    }
758
759    /// Sets the global `core:metadata_only` field: whether the recording
760    /// intentionally has no dataset file.
761    ///
762    /// Clears the retained [original bytes](Metadata::original_bytes).
763    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    /// Sets the global `core:meta_doi` field: the DOI of the metadata file.
769    ///
770    /// Clears the retained [original bytes](Metadata::original_bytes).
771    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    /// Sets the global `core:num_channels` field: the number of interleaved
777    /// channels in the dataset.
778    ///
779    /// Clears the retained [original bytes](Metadata::original_bytes).
780    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    /// Sets the global `core:offset` field: the index offset of the first
786    /// sample in the dataset.
787    ///
788    /// Clears the retained [original bytes](Metadata::original_bytes).
789    pub fn set_offset(&mut self, offset: Integer) {
790        self.original_bytes = None;
791        self.global.set_offset(offset);
792    }
793
794    /// Sets the global `core:recorder` field: the name of the software that
795    /// made the recording.
796    ///
797    /// Clears the retained [original bytes](Metadata::original_bytes).
798    pub fn set_recorder(&mut self, recorder: String) {
799        self.original_bytes = None;
800        self.global.set_recorder(recorder);
801    }
802
803    /// Sets the global `core:sha512` field: the SHA-512 digest of the dataset
804    /// file.
805    ///
806    /// Clears the retained [original bytes](Metadata::original_bytes).
807    pub fn set_sha512(&mut self, hash: String) {
808        self.original_bytes = None;
809        self.global.set_sha512(hash);
810    }
811
812    /// Sets the global `core:geolocation` field: the GeoJSON location of the
813    /// recording.
814    ///
815    /// Clears the retained [original bytes](Metadata::original_bytes).
816    pub fn set_geolocation(&mut self, geolocation: GeoJson) {
817        self.original_bytes = None;
818        self.global.set_geolocation(geolocation);
819    }
820
821    /// Sets the global `core:extensions` field: the list of extension
822    /// namespaces used by the recording.
823    ///
824    /// Clears the retained [original bytes](Metadata::original_bytes).
825    pub fn set_extensions(&mut self, extensions: Vec<global::ExtensionDeclaration>) {
826        self.original_bytes = None;
827        self.global.set_extensions(extensions);
828    }
829
830    /// Sets the global `core:dataset` field: the file name of a non-conforming
831    /// dataset.
832    ///
833    /// Clears the retained [original bytes](Metadata::original_bytes).
834    pub fn set_dataset(&mut self, dataset: String) {
835        self.original_bytes = None;
836        self.global.set_dataset(dataset);
837    }
838
839    /// Sets the global `core:trailing_bytes` field: the number of non-sample
840    /// bytes at the end of the dataset file.
841    ///
842    /// Clears the retained [original bytes](Metadata::original_bytes).
843    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    // ---------------- Annotations ---------------- //
849    /// Appends an entry to the `annotations` array.
850    ///
851    /// No ordering is enforced. Clears the retained
852    /// [original bytes](Metadata::original_bytes).
853    pub fn add_annotation(&mut self, annotation: annotation::Annotation) {
854        self.original_bytes = None;
855        self.annotations.push(annotation);
856    }
857
858    // ----------------- Captures ------------------ //
859    /// Appends an entry to the `captures` array.
860    ///
861    /// No ordering is enforced. Clears the retained
862    /// [original bytes](Metadata::original_bytes).
863    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    // ================================== Getters ================================== //
989    // ---------------- Global ---------------- //
990    /// Returns the global `core:version` field: the SigMF specification version
991    /// string.
992    pub fn get_version(&self) -> String {
993        self.global.get_version()
994    }
995
996    /// Returns the global `core:datatype` field: the sample format of the
997    /// dataset.
998    pub fn get_datatype(&self) -> global::Datatype {
999        self.global.get_datatype()
1000    }
1001
1002    /// Returns the global `core:sample_rate` field: the sample rate in samples
1003    /// per second, if set.
1004    pub fn get_sample_rate(&self) -> Option<Number> {
1005        self.global.get_sample_rate()
1006    }
1007
1008    /// Returns the global `core:author` field: the author of the recording, if
1009    /// set.
1010    pub fn get_author(&self) -> Option<String> {
1011        self.global.get_author()
1012    }
1013
1014    /// Returns the global `core:collection` field: the base name of the SigMF
1015    /// collection this recording belongs to, if set.
1016    pub fn get_collection(&self) -> Option<String> {
1017        self.global.get_collection()
1018    }
1019
1020    /// Returns the global `core:data_doi` field: the DOI of the dataset file,
1021    /// if set.
1022    pub fn get_data_doi(&self) -> Option<String> {
1023        self.global.get_data_doi()
1024    }
1025
1026    /// Returns the global `core:description` field: the free-text description
1027    /// of the recording, if set.
1028    pub fn get_description(&self) -> Option<String> {
1029        self.global.get_description()
1030    }
1031
1032    /// Returns the global `core:hw` field: the description of the recording
1033    /// hardware, if set.
1034    pub fn get_hw(&self) -> Option<String> {
1035        self.global.get_hw()
1036    }
1037
1038    /// Returns the global `core:license` field: the URL of the license
1039    /// governing the recording, if set.
1040    pub fn get_license(&self) -> Option<String> {
1041        self.global.get_license()
1042    }
1043
1044    /// Returns the global `core:metadata_only` field: whether the recording
1045    /// intentionally has no dataset file, if set.
1046    pub fn get_metadata_only(&self) -> Option<bool> {
1047        self.global.get_metadata_only()
1048    }
1049
1050    /// Returns the global `core:meta_doi` field: the DOI of the metadata file,
1051    /// if set.
1052    pub fn get_meta_doi(&self) -> Option<String> {
1053        self.global.get_meta_doi()
1054    }
1055
1056    /// Returns the global `core:num_channels` field: the number of interleaved
1057    /// channels in the dataset, if set.
1058    pub fn get_num_channels(&self) -> Option<Integer> {
1059        self.global.get_num_channels()
1060    }
1061
1062    /// Returns the global `core:offset` field: the index offset of the first
1063    /// sample in the dataset, if set.
1064    pub fn get_offset(&self) -> Option<Integer> {
1065        self.global.get_offset()
1066    }
1067
1068    /// Returns the global `core:recorder` field: the name of the software that
1069    /// made the recording, if set.
1070    pub fn get_recorder(&self) -> Option<String> {
1071        self.global.get_recorder()
1072    }
1073
1074    /// Returns the global `core:sha512` field: the SHA-512 digest of the
1075    /// dataset file, if set.
1076    pub fn get_sha512(&self) -> Option<String> {
1077        self.global.get_sha512()
1078    }
1079
1080    /// Returns the global `core:geolocation` field: the GeoJSON location of the
1081    /// recording, if set.
1082    pub fn get_geolocation(&self) -> Option<GeoJson> {
1083        self.global.get_geolocation()
1084    }
1085
1086    /// Returns the global `core:extensions` field: the list of extension
1087    /// namespaces used by the recording, if set.
1088    pub fn get_extensions(&self) -> Option<Vec<global::ExtensionDeclaration>> {
1089        self.global.get_extensions()
1090    }
1091
1092    /// Returns the global `core:dataset` field: the file name of a
1093    /// non-conforming dataset, if set.
1094    pub fn get_dataset(&self) -> Option<String> {
1095        self.global.get_dataset()
1096    }
1097
1098    /// Returns the global `core:trailing_bytes` field: the number of non-sample
1099    /// bytes at the end of the dataset file, if set.
1100    pub fn get_trailing_bytes(&self) -> Option<Integer> {
1101        self.global.get_trailing_bytes()
1102    }
1103
1104    // ---------------- Annotations ---------------- //
1105    /// Returns a clone of the `annotations` array.
1106    ///
1107    /// Prefer [`Metadata::annotations`] to borrow without cloning.
1108    pub fn get_annotations(&self) -> Vec<annotation::Annotation> {
1109        self.annotations.clone()
1110    }
1111
1112    // ----------------- Captures ------------------ //
1113    /// Returns a clone of the `captures` array.
1114    ///
1115    /// Prefer [`Metadata::captures`] to borrow without cloning.
1116    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/// A single leaf-level difference reported by [`Metadata::diff`].
1149#[derive(Debug, Clone, PartialEq)]
1150pub struct MetadataChange {
1151    /// RFC 6901 JSON Pointer to the changed value (for example
1152    /// `/global/core:description`), or `/` for the document root.
1153    pub json_pointer: String,
1154    /// The value in the original document, or `None` if it was added.
1155    pub before: Option<Value>,
1156    /// The value in the other document, or `None` if it was removed.
1157    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
1199/// A transactional edit of a [`Metadata`] document, created by
1200/// [`Metadata::edit`].
1201///
1202/// Edits are applied to a draft. To publish them, call
1203/// [`MetadataEdit::validate`] and then [`MetadataEdit::commit`]; any change
1204/// made after validating invalidates the report, so `commit` always applies
1205/// exactly the draft that was validated. Dropping the edit discards the
1206/// draft.
1207///
1208/// # Examples
1209///
1210/// ```rust
1211/// use sigmf::meta::Metadata;
1212/// use sigmf::validation::ValidationPolicy;
1213///
1214/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
1215/// let mut metadata = Metadata::from_slice(br#"{
1216///     "global": {"core:version": "1.2.6", "core:datatype": "ri16_le"},
1217///     "captures": [],
1218///     "annotations": []
1219/// }"#)?;
1220///
1221/// let mut edit = metadata.edit();
1222/// edit.set_description("reviewed".to_string());
1223/// edit.validate(ValidationPolicy::StrictSupportedVersion);
1224/// edit.commit()?;
1225///
1226/// assert_eq!(metadata.get_description().as_deref(), Some("reviewed"));
1227/// # Ok(())
1228/// # }
1229/// ```
1230pub struct MetadataEdit<'a> {
1231    target: &'a mut Metadata,
1232    draft: Metadata,
1233    validation: Option<crate::validation::ValidationReport>,
1234}
1235
1236impl MetadataEdit<'_> {
1237    /// Returns the current draft.
1238    pub fn metadata(&self) -> &Metadata {
1239        &self.draft
1240    }
1241
1242    /// Returns mutable access to the draft for edits not covered by the
1243    /// convenience methods.
1244    ///
1245    /// Discards any validation report, so [`MetadataEdit::validate`] must be
1246    /// called again before committing.
1247    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    /// Sets the draft's global `core:description` field and discards any
1254    /// validation report.
1255    pub fn set_description(&mut self, description: String) {
1256        self.draft.set_description(description);
1257        self.validation = None;
1258    }
1259
1260    /// Sets the draft's global `core:datatype` field and discards any
1261    /// validation report.
1262    pub fn set_datatype(&mut self, datatype: global::Datatype) {
1263        self.draft.set_datatype(datatype);
1264        self.validation = None;
1265    }
1266
1267    /// Sets the draft's global `core:offset` field and discards any
1268    /// validation report.
1269    pub fn set_offset(&mut self, offset: Integer) {
1270        self.draft.set_offset(offset);
1271        self.validation = None;
1272    }
1273
1274    /// Appends an annotation to the draft and discards any validation report.
1275    pub fn add_annotation(&mut self, annotation: annotation::Annotation) {
1276        self.draft.add_annotation(annotation);
1277        self.validation = None;
1278    }
1279
1280    /// Replaces the draft annotation at `index`, returning the old one.
1281    ///
1282    /// Returns `None` and leaves the draft (and any validation report)
1283    /// unchanged if `index` is out of bounds.
1284    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    /// Appends a capture to the draft and discards any validation report.
1295    pub fn add_capture(&mut self, capture: capture::Capture) {
1296        self.draft.add_capture(capture);
1297        self.validation = None;
1298    }
1299
1300    /// Replaces the draft capture at `index`, returning the old one.
1301    ///
1302    /// Returns `None` and leaves the draft (and any validation report)
1303    /// unchanged if `index` is out of bounds.
1304    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    /// Removes and returns the draft annotation at `index`, shifting later
1315    /// annotations down.
1316    ///
1317    /// Returns `None` and leaves the draft (and any validation report)
1318    /// unchanged if `index` is out of bounds.
1319    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    /// Removes and returns the draft capture at `index`, shifting later
1329    /// captures down.
1330    ///
1331    /// Returns `None` and leaves the draft (and any validation report)
1332    /// unchanged if `index` is out of bounds.
1333    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    /// Stably sorts the draft's captures and annotations by
1343    /// `core:sample_start`, as SigMF requires, and discards any validation
1344    /// report.
1345    ///
1346    /// Entries with equal `core:sample_start` keep their relative order.
1347    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    /// Validates the draft under `policy` and records the report for
1358    /// [`MetadataEdit::commit`].
1359    ///
1360    /// The returned report is stamped with the policy and a fingerprint of
1361    /// the draft, so it becomes stale if the draft changes afterwards. See
1362    /// [`Metadata::validate`] for the checks performed.
1363    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    /// Applies the draft to the original [`Metadata`], consuming the edit.
1376    ///
1377    /// # Errors
1378    ///
1379    /// Returns [`crate::SigmfError::InvalidMetadata`], leaving the original
1380    /// document unchanged, if:
1381    ///
1382    /// * [`MetadataEdit::validate`] has not been called since the last
1383    ///   change;
1384    /// * the recorded report has status
1385    ///   [`Failed`](crate::validation::ValidationStatus::Failed); or
1386    /// * the draft no longer matches the fingerprint in the report.
1387    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}