1use crate::meta::Metadata;
28use crate::utils::data::{self, SampleValue, SampleWriter};
29use crate::{Integer, SigmfError};
30use std::fs::File;
31use std::io::{Read, Seek, SeekFrom, Write};
32use std::path::{Path, PathBuf};
33
34struct DatasetLayout {
35 frame_count: Integer,
36 bytes_per_frame: u64,
37 header_bytes: Vec<(Integer, Integer)>,
38}
39
40pub struct RecordingWriter {
105 metadata: Metadata,
106 metadata_path: PathBuf,
107 dataset_path: PathBuf,
108 temporary_dataset_path: PathBuf,
109 sample_writer: Option<SampleWriter<File>>,
110 dataset_published: bool,
111 metadata_published: bool,
112}
113
114impl RecordingWriter {
115 pub fn new(metadata_path: impl AsRef<Path>, metadata: Metadata) -> Result<Self, SigmfError> {
139 let metadata_path = metadata_path.as_ref().to_path_buf();
140 if metadata_path
141 .extension()
142 .and_then(|extension| extension.to_str())
143 != Some("sigmf-meta")
144 {
145 return Err(SigmfError::InvalidMetadata(
146 "recording metadata path must end with .sigmf-meta".into(),
147 ));
148 }
149 if metadata.global().metadata_only == Some(true) {
150 return Err(SigmfError::UnsupportedLayout(
151 "metadata-only recordings cannot be created with sample data".into(),
152 ));
153 }
154 if metadata
155 .captures()
156 .iter()
157 .any(|capture| capture.header_bytes.unwrap_or(0) > 0)
158 || metadata.global().trailing_bytes.unwrap_or(0) > 0
159 {
160 return Err(SigmfError::UnsupportedLayout(
161 "recording writer does not synthesize NCD headers or trailing bytes".into(),
162 ));
163 }
164 if metadata
165 .validate(crate::validation::ValidationPolicy::Interoperability)
166 .status
167 == crate::validation::ValidationStatus::Failed
168 {
169 return Err(SigmfError::InvalidMetadata(
170 "recording metadata failed validation".into(),
171 ));
172 }
173 let dataset_path = dataset_path_for_metadata(&metadata_path, &metadata)?;
174 if dataset_path == metadata_path {
175 return Err(SigmfError::InvalidMetadata(
176 "dataset and metadata paths must be different".into(),
177 ));
178 }
179 if metadata_path.try_exists()? || dataset_path.try_exists()? {
180 return Err(SigmfError::InvalidMetadata(
181 "recording writer does not overwrite existing files".into(),
182 ));
183 }
184 let channel_count =
185 usize::try_from(metadata.global().num_channels.unwrap_or(1)).map_err(|_| {
186 SigmfError::InvalidMetadata("core:num_channels does not fit usize".into())
187 })?;
188 let (temporary_dataset_path, dataset_file) =
189 crate::persistence::create_sibling_temp(&dataset_path)?;
190 let sample_writer = match SampleWriter::new(
191 dataset_file,
192 metadata.global().datatype.clone(),
193 channel_count,
194 ) {
195 Ok(writer) => writer,
196 Err(error) => {
197 let _ = std::fs::remove_file(&temporary_dataset_path);
198 return Err(error);
199 }
200 };
201 Ok(Self {
202 metadata,
203 metadata_path,
204 dataset_path,
205 temporary_dataset_path,
206 sample_writer: Some(sample_writer),
207 dataset_published: false,
208 metadata_published: false,
209 })
210 }
211
212 pub fn write_frames(&mut self, samples: &[SampleValue]) -> Result<(), SigmfError> {
223 self.sample_writer
224 .as_mut()
225 .ok_or_else(|| {
226 SigmfError::InvalidMetadata("recording writer is already finalized".into())
227 })?
228 .write_frames(samples)
229 }
230
231 pub fn finish(mut self) -> Result<String, SigmfError> {
249 let file = self
250 .sample_writer
251 .take()
252 .ok_or_else(|| {
253 SigmfError::InvalidMetadata("recording writer is already finalized".into())
254 })?
255 .finish()?;
256 file.sync_all()?;
257 drop(file);
258 let mut file = File::open(&self.temporary_dataset_path)?;
259 let digest = crate::utils::hash::hash_from_reader(&mut file)?;
260 drop(file);
261
262 self.metadata.set_sha512(digest.clone());
263 if self
264 .metadata
265 .validate(crate::validation::ValidationPolicy::Interoperability)
266 .status
267 == crate::validation::ValidationStatus::Failed
268 {
269 return Err(SigmfError::InvalidMetadata(
270 "recording metadata failed validation after digest update".into(),
271 ));
272 }
273 let metadata_bytes = self.metadata.to_vec()?;
274 if metadata_bytes.len() > crate::MAX_META_SIZE {
275 return Err(SigmfError::ResourceLimit(format!(
276 "metadata exceeds the {} byte limit",
277 crate::MAX_META_SIZE
278 )));
279 }
280 let (temporary_metadata_path, mut metadata_file) =
281 crate::persistence::create_sibling_temp(&self.metadata_path)?;
282 let _metadata_guard = TemporaryFilePath(temporary_metadata_path.clone());
283 metadata_file.write_all(&metadata_bytes)?;
284 metadata_file.flush()?;
285 metadata_file.sync_all()?;
286 drop(metadata_file);
287
288 if let Err(error) =
289 crate::persistence::publish_new(&self.temporary_dataset_path, &self.dataset_path)
290 {
291 if crate::utils::hash::hash_from_path(&self.dataset_path)
292 .is_ok_and(|published_digest| published_digest == digest)
293 {
294 self.dataset_published = true;
295 }
296 return Err(error);
297 }
298 self.dataset_published = true;
299 if let Err(error) =
300 crate::persistence::publish_new(&temporary_metadata_path, &self.metadata_path)
301 {
302 if std::fs::read(&self.metadata_path).is_ok_and(|bytes| bytes == metadata_bytes) {
303 self.metadata_published = true;
304 return Err(error);
305 }
306 match std::fs::remove_file(&self.dataset_path) {
307 Ok(()) => self.dataset_published = false,
308 Err(cleanup_error) => {
309 return Err(SigmfError::InvalidMetadata(format!(
310 "metadata publication failed ({error}) and dataset cleanup failed ({}): {cleanup_error}",
311 self.dataset_path.display()
312 )));
313 }
314 }
315 return Err(error);
316 }
317 self.metadata_published = true;
318 Ok(digest)
319 }
320}
321
322impl Drop for RecordingWriter {
323 fn drop(&mut self) {
324 drop(self.sample_writer.take());
325 if self.dataset_published && !self.metadata_published {
326 let _ = std::fs::remove_file(&self.dataset_path);
327 }
328 let _ = std::fs::remove_file(&self.temporary_dataset_path);
329 }
330}
331
332struct TemporaryFilePath(PathBuf);
333
334impl Drop for TemporaryFilePath {
335 fn drop(&mut self) {
336 let _ = std::fs::remove_file(&self.0);
337 }
338}
339
340fn dataset_path_for_metadata(
341 metadata_path: &Path,
342 metadata: &Metadata,
343) -> Result<PathBuf, SigmfError> {
344 let filename = metadata_path
345 .file_name()
346 .and_then(|filename| filename.to_str())
347 .ok_or_else(|| {
348 SigmfError::InvalidMetadata("metadata filename must be valid UTF-8".into())
349 })?;
350 let stem = filename.strip_suffix(".sigmf-meta").ok_or_else(|| {
351 SigmfError::InvalidMetadata("metadata path must end with .sigmf-meta".into())
352 })?;
353 let dataset_name = metadata
354 .global()
355 .dataset
356 .clone()
357 .unwrap_or_else(|| format!("{stem}.sigmf-data"));
358 crate::validate_dataset_filename(&dataset_name)?;
359 Ok(metadata_path
360 .parent()
361 .filter(|parent| !parent.as_os_str().is_empty())
362 .unwrap_or_else(|| Path::new("."))
363 .join(dataset_name))
364}
365
366pub struct Recording {
372 metadata: Metadata,
373 metadata_path: PathBuf,
374 dataset_path: Option<PathBuf>,
375 dataset: Option<File>,
376 dataset_bytes: u64,
377}
378
379impl Recording {
380 pub fn open(path: impl AsRef<Path>) -> Result<Self, SigmfError> {
395 Self::open_inner(path.as_ref(), true)
396 }
397
398 pub fn open_metadata_only(path: impl AsRef<Path>) -> Result<Self, SigmfError> {
408 Self::open_inner(path.as_ref(), false)
409 }
410
411 fn open_inner(path: &Path, open_dataset: bool) -> Result<Self, SigmfError> {
412 let metadata = Metadata::from_path(path)?;
413 let dataset_name = metadata.global().dataset.clone();
414 let dataset_path = match dataset_name.as_deref() {
415 Some(name) => {
416 if name.is_empty()
417 || name.contains('/')
418 || name.contains('\\')
419 || Path::new(name).components().count() != 1
420 || !matches!(
421 Path::new(name).components().next(),
422 Some(std::path::Component::Normal(_))
423 )
424 {
425 return Err(SigmfError::InvalidMetadata(
426 "core:dataset must be a filename in the metadata directory".into(),
427 ));
428 }
429 path.parent().unwrap_or_else(|| Path::new("")).join(name)
430 }
431 None => {
432 let mut dataset_path = path.to_path_buf();
433 dataset_path.set_extension("sigmf-data");
434 dataset_path
435 }
436 };
437 let metadata_only = metadata.global().metadata_only == Some(true);
438 let should_open_dataset =
439 open_dataset && (!metadata_only || (dataset_name.is_some() && dataset_path.is_file()));
440 let (dataset, actual_dataset_path, dataset_bytes) = if should_open_dataset {
441 let file = File::open(&dataset_path)?;
442 let dataset_bytes = file.metadata()?.len();
443 (Some(file), Some(dataset_path), dataset_bytes)
444 } else {
445 (None, None, 0)
446 };
447
448 Ok(Self {
449 metadata,
450 metadata_path: path.to_path_buf(),
451 dataset_path: actual_dataset_path,
452 dataset,
453 dataset_bytes,
454 })
455 }
456
457 pub fn metadata(&self) -> &Metadata {
459 &self.metadata
460 }
461
462 pub fn metadata_path(&self) -> &Path {
464 &self.metadata_path
465 }
466
467 pub fn dataset_path(&self) -> Option<&Path> {
470 self.dataset_path.as_deref()
471 }
472
473 pub fn first_sample_index(&self) -> Integer {
476 self.metadata.global().offset.unwrap_or(0)
477 }
478
479 pub fn frame_count(&self) -> Result<Integer, SigmfError> {
492 Ok(self.layout()?.frame_count)
493 }
494
495 pub fn dataset_digest(&mut self) -> Result<String, SigmfError> {
503 let dataset = self.dataset.as_mut().ok_or(SigmfError::NoDataset)?;
504 dataset.seek(SeekFrom::Start(0))?;
505 crate::utils::hash::hash_from_reader(dataset)
506 }
507
508 pub fn verify_dataset_digest(&mut self) -> Result<(), SigmfError> {
518 let expected = self
519 .metadata
520 .global()
521 .sha512
522 .clone()
523 .ok_or_else(|| SigmfError::InvalidMetadata("core:sha512 is not present".into()))?;
524 let actual = self.dataset_digest()?;
525 if !actual.eq_ignore_ascii_case(&expected) {
526 return Err(SigmfError::HashMismatch(format!(
527 "dataset digest mismatch: expected {expected}, found {actual}"
528 )));
529 }
530 Ok(())
531 }
532
533 fn layout(&self) -> Result<DatasetLayout, SigmfError> {
534 if self.dataset.is_none() {
535 return Err(SigmfError::NoDataset);
536 }
537 let global = self.metadata.global();
538 let trailer = global.trailing_bytes.unwrap_or(0);
539 if trailer > self.dataset_bytes {
540 return Err(SigmfError::UnsupportedLayout(
541 "core:trailing_bytes exceeds the dataset file size".to_string(),
542 ));
543 }
544 let channels = global.num_channels.unwrap_or(1);
545 let channels = usize::try_from(channels).map_err(|_| {
546 SigmfError::InvalidMetadata("core:num_channels does not fit usize".to_string())
547 })?;
548 let bytes_per_frame = u64::try_from(data::bytes_per_frame(&global.datatype, channels)?)
549 .map_err(|_| {
550 SigmfError::ResourceLimit("frame size does not fit the file address space".into())
551 })?;
552 let first_sample_index = self.first_sample_index();
553 let mut header_bytes: Vec<_> = self
554 .metadata
555 .captures()
556 .iter()
557 .filter_map(|capture| {
558 let bytes = capture.header_bytes.unwrap_or(0);
559 if bytes > 0 && capture.sample_start >= first_sample_index {
560 Some((capture.sample_start - first_sample_index, bytes))
561 } else {
562 None
563 }
564 })
565 .collect();
566 header_bytes.sort_by_key(|(boundary, _)| *boundary);
567 let total_header_bytes = header_bytes.iter().try_fold(0u64, |total, (_, bytes)| {
568 total.checked_add(*bytes).ok_or_else(|| {
569 SigmfError::UnsupportedLayout("capture header size overflows".into())
570 })
571 })?;
572 let sample_bytes = self
573 .dataset_bytes
574 .checked_sub(trailer)
575 .and_then(|bytes| bytes.checked_sub(total_header_bytes))
576 .ok_or_else(|| {
577 SigmfError::UnsupportedLayout(
578 "capture headers and trailing bytes exceed dataset size".into(),
579 )
580 })?;
581 let frame_count = data::frame_count(sample_bytes, &global.datatype, channels)?;
582 if header_bytes
583 .iter()
584 .any(|(boundary, _)| *boundary > frame_count)
585 {
586 return Err(SigmfError::UnsupportedLayout(
587 "capture header starts beyond the dataset sample range".into(),
588 ));
589 }
590 Ok(DatasetLayout {
591 frame_count,
592 bytes_per_frame,
593 header_bytes,
594 })
595 }
596
597 pub fn read_frames(
613 &mut self,
614 start: Integer,
615 count: Integer,
616 ) -> Result<Vec<SampleValue>, SigmfError> {
617 let layout = self.layout()?;
618 let first_sample_index = self.first_sample_index();
619 let relative_start = start.checked_sub(first_sample_index).ok_or_else(|| {
620 SigmfError::InvalidRange(format!(
621 "requested sample index {start} precedes dataset offset {first_sample_index}"
622 ))
623 })?;
624 let relative_end = relative_start.checked_add(count).ok_or_else(|| {
625 SigmfError::InvalidRange("requested frame range overflows".to_string())
626 })?;
627 if relative_end > layout.frame_count {
628 return Err(SigmfError::InvalidRange(format!(
629 "requested sample range {start}..+{count} exceeds dataset range starting at {first_sample_index} with {} frames",
630 layout.frame_count
631 )));
632 }
633 let global = self.metadata.global();
634 let channels = usize::try_from(global.num_channels.unwrap_or(1)).map_err(|_| {
635 SigmfError::InvalidMetadata("core:num_channels does not fit usize".to_string())
636 })?;
637 let dataset = self.dataset.as_mut().ok_or(SigmfError::NoDataset)?;
638 let sample_count = usize::try_from(count)
639 .ok()
640 .and_then(|count| count.checked_mul(channels))
641 .ok_or_else(|| {
642 SigmfError::ResourceLimit("requested frame block does not fit memory".into())
643 })?;
644 let mut samples = Vec::with_capacity(sample_count);
645 let mut current = relative_start;
646 while current < relative_end {
647 let next_boundary = layout
648 .header_bytes
649 .iter()
650 .find_map(|(boundary, _)| (*boundary > current).then_some(*boundary))
651 .unwrap_or(relative_end)
652 .min(relative_end);
653 let chunk_frames = next_boundary - current;
654 let samples_before = current
655 .checked_mul(layout.bytes_per_frame)
656 .ok_or_else(|| SigmfError::InvalidRange("byte offset overflow".into()))?;
657 let headers_before = layout
658 .header_bytes
659 .iter()
660 .filter(|(boundary, _)| *boundary <= current)
661 .try_fold(0u64, |total, (_, bytes)| total.checked_add(*bytes))
662 .ok_or_else(|| SigmfError::InvalidRange("header offset overflow".into()))?;
663 let byte_offset = samples_before
664 .checked_add(headers_before)
665 .ok_or_else(|| SigmfError::InvalidRange("byte offset overflow".into()))?;
666 let byte_len = chunk_frames
667 .checked_mul(layout.bytes_per_frame)
668 .and_then(|length| usize::try_from(length).ok())
669 .ok_or_else(|| {
670 SigmfError::ResourceLimit("requested frame block does not fit memory".into())
671 })?;
672 dataset.seek(SeekFrom::Start(byte_offset))?;
673 let mut bytes = vec![0; byte_len];
674 dataset.read_exact(&mut bytes)?;
675 samples.extend(data::decode(&bytes, &global.datatype, channels)?);
676 current = next_boundary;
677 }
678 Ok(samples)
679 }
680
681 pub fn frame_blocks(
694 &mut self,
695 start: Integer,
696 count: Integer,
697 block_frames: Integer,
698 ) -> Result<FrameBlocks<'_>, SigmfError> {
699 if block_frames == 0 {
700 return Err(SigmfError::InvalidRange(
701 "block size must be greater than zero".to_string(),
702 ));
703 }
704 let frame_count = self.frame_count()?;
705 let first_sample_index = self.first_sample_index();
706 let relative_start = start.checked_sub(first_sample_index).ok_or_else(|| {
707 SigmfError::InvalidRange(format!(
708 "requested sample index {start} precedes dataset offset {first_sample_index}"
709 ))
710 })?;
711 let relative_end = relative_start.checked_add(count).ok_or_else(|| {
712 SigmfError::InvalidRange("requested frame range overflows".to_string())
713 })?;
714 if relative_end > frame_count {
715 return Err(SigmfError::InvalidRange(format!(
716 "requested sample range {start}..+{count} exceeds dataset range starting at {first_sample_index} with {frame_count} frames"
717 )));
718 }
719
720 Ok(FrameBlocks {
721 recording: self,
722 first_sample_index,
723 next_relative: relative_start,
724 remaining: count,
725 block_frames,
726 })
727 }
728
729 pub fn crop_to(
752 &mut self,
753 metadata_path: impl AsRef<Path>,
754 sample_start: Integer,
755 sample_count: Integer,
756 block_frames: Integer,
757 extension_handler: Option<&mut dyn crate::extensions::CropExtensionHandler>,
758 ) -> Result<String, SigmfError> {
759 if block_frames == 0 {
760 return Err(SigmfError::InvalidRange(
761 "block size must be greater than zero".into(),
762 ));
763 }
764 {
765 let _validated_blocks = self.frame_blocks(sample_start, sample_count, block_frames)?;
766 }
767 let metadata = self
768 .metadata
769 .crop_samples(sample_start, sample_count, extension_handler)?;
770 let blocks = self.frame_blocks(sample_start, sample_count, block_frames)?;
771 let mut writer = RecordingWriter::new(metadata_path, metadata)?;
772 for block in blocks {
773 writer.write_frames(&block?)?;
774 }
775 writer.finish()
776 }
777}
778
779pub struct FrameBlocks<'a> {
784 recording: &'a mut Recording,
785 first_sample_index: Integer,
786 next_relative: Integer,
787 remaining: Integer,
788 block_frames: Integer,
789}
790
791impl Iterator for FrameBlocks<'_> {
792 type Item = Result<Vec<SampleValue>, SigmfError>;
793
794 fn next(&mut self) -> Option<Self::Item> {
795 if self.remaining == 0 {
796 return None;
797 }
798 let count = self.remaining.min(self.block_frames);
799 let start = match self.first_sample_index.checked_add(self.next_relative) {
800 Some(start) => start,
801 None => {
802 self.remaining = 0;
803 return Some(Err(SigmfError::InvalidRange(
804 "absolute sample index overflows".to_string(),
805 )));
806 }
807 };
808 match self.recording.read_frames(start, count) {
809 Ok(samples) => {
810 self.next_relative += count;
811 self.remaining -= count;
812 Some(Ok(samples))
813 }
814 Err(error) => {
815 self.remaining = 0;
816 Some(Err(error))
817 }
818 }
819 }
820}
821
822#[cfg(test)]
823mod tests {
824 use super::*;
825 use crate::annotation::Annotation;
826 use crate::capture::Capture;
827 use crate::datatype::Datatype;
828 use crate::global::Global;
829 use crate::meta::Metadata;
830 use std::str::FromStr;
831
832 fn write_recording(root: &Path, datatype: &str, data: &[u8]) -> PathBuf {
833 std::fs::create_dir_all(root).unwrap();
834 let metadata_path = root.join("capture.sigmf-meta");
835 let metadata = Metadata::new(
836 Global::new("1.2.6", Datatype::from_str(datatype).unwrap()),
837 Vec::new(),
838 Vec::new(),
839 );
840 std::fs::write(&metadata_path, metadata.to_vec().unwrap()).unwrap();
841 std::fs::write(root.join("capture.sigmf-data"), data).unwrap();
842 metadata_path
843 }
844
845 #[test]
846 fn crop_writes_bounded_plain_data_and_clips_absolute_metadata_ranges() {
847 let root = std::env::temp_dir().join(format!(
848 "sigmf-recording-crop-test-{}-{}",
849 std::process::id(),
850 std::time::SystemTime::now()
851 .duration_since(std::time::UNIX_EPOCH)
852 .unwrap()
853 .as_nanos()
854 ));
855 std::fs::create_dir_all(&root).unwrap();
856 let source_path = root.join("source.sigmf-meta");
857 let output_path = root.join("cropped.sigmf-meta");
858
859 let mut global = Global::new("1.2.6", Datatype::from_str("ru8").unwrap());
860 global.offset = Some(10);
861 global.trailing_bytes = Some(1);
862 let mut first_capture = Capture::new(10);
863 first_capture.header_bytes = Some(1);
864 let mut second_capture = Capture::new(12);
865 second_capture.header_bytes = Some(1);
866 let metadata = Metadata::new(
867 global,
868 vec![
869 Annotation::new(9, Some(3), None, None, None, None, None, None),
870 Annotation::new(12, Some(4), None, None, None, None, None, None),
871 Annotation::new(16, None, None, None, None, None, None, None),
872 ],
873 vec![first_capture, second_capture],
874 );
875 std::fs::write(&source_path, metadata.to_vec().unwrap()).unwrap();
876 std::fs::write(
877 root.join("source.sigmf-data"),
878 [0xaa, 1, 2, 0xbb, 3, 4, 0xcc],
879 )
880 .unwrap();
881
882 let mut recording = Recording::open(source_path).unwrap();
883 recording.crop_to(&output_path, 11, 2, 1, None).unwrap();
884
885 let mut cropped = Recording::open(&output_path).unwrap();
886 assert_eq!(cropped.first_sample_index(), 11);
887 assert_eq!(cropped.frame_count().unwrap(), 2);
888 assert_eq!(
889 cropped.read_frames(11, 2).unwrap(),
890 vec![
891 SampleValue::RealUnsignedBytes(2),
892 SampleValue::RealUnsignedBytes(3)
893 ]
894 );
895 cropped.verify_dataset_digest().unwrap();
896 assert_eq!(
897 std::fs::read(root.join("cropped.sigmf-data")).unwrap(),
898 [2, 3]
899 );
900 assert_eq!(
901 cropped
902 .metadata()
903 .captures()
904 .iter()
905 .map(|capture| (capture.sample_start, capture.header_bytes))
906 .collect::<Vec<_>>(),
907 vec![(10, None), (12, None)]
908 );
909 assert_eq!(
910 cropped
911 .metadata()
912 .annotations()
913 .iter()
914 .map(|annotation| (annotation.sample_start, annotation.sample_count))
915 .collect::<Vec<_>>(),
916 vec![(11, Some(1)), (12, Some(1))]
917 );
918 assert_eq!(cropped.metadata().global().trailing_bytes, None);
919 std::fs::remove_dir_all(root).unwrap();
920 }
921
922 #[test]
923 fn crop_requires_extension_handler_for_extension_fields() {
924 struct ShiftExtensionCoordinates;
925
926 impl crate::extensions::CropExtensionHandler for ShiftExtensionCoordinates {
927 fn transform_for_crop(
928 &mut self,
929 scope: crate::extensions::ExtensionScope,
930 sample_start: Option<Integer>,
931 _sample_count: Option<Integer>,
932 crop_start: Integer,
933 _crop_count: Integer,
934 fields: &mut serde_json::Map<String, serde_json::Value>,
935 ) -> Result<(), SigmfError> {
936 assert_eq!(scope, crate::extensions::ExtensionScope::Capture);
937 assert_eq!(sample_start, Some(0));
938 fields.insert("lab:sample_start".into(), serde_json::json!(crop_start));
939 Ok(())
940 }
941 }
942
943 let root = std::env::temp_dir().join(format!(
944 "sigmf-recording-crop-extension-test-{}-{}",
945 std::process::id(),
946 std::time::SystemTime::now()
947 .duration_since(std::time::UNIX_EPOCH)
948 .unwrap()
949 .as_nanos()
950 ));
951 std::fs::create_dir_all(&root).unwrap();
952 let source_path = write_recording(&root, "ru8", &[1, 2]);
953 let mut metadata = Metadata::from_path(&source_path).unwrap();
954 let mut capture = Capture::new(0);
955 capture
956 .insert_extra("lab:sample_start", serde_json::json!(0))
957 .unwrap();
958 metadata.add_capture(capture);
959 std::fs::write(&source_path, metadata.to_vec().unwrap()).unwrap();
960
961 let mut recording = Recording::open(&source_path).unwrap();
962 let output_path = root.join("without-handler.sigmf-meta");
963 assert!(recording.crop_to(&output_path, 1, 1, 1, None).is_err());
964 assert!(!output_path.exists());
965
966 let output_path = root.join("with-handler.sigmf-meta");
967 recording
968 .crop_to(&output_path, 1, 1, 1, Some(&mut ShiftExtensionCoordinates))
969 .unwrap();
970 let cropped = Metadata::from_path(output_path).unwrap();
971 assert_eq!(
972 cropped.captures()[0].extra_fields()["lab:sample_start"],
973 serde_json::json!(1)
974 );
975 std::fs::remove_dir_all(root).unwrap();
976 }
977
978 #[test]
979 fn reads_requested_frames_relative_to_metadata_path() {
980 let root = std::env::temp_dir().join(format!(
981 "sigmf-recording-test-{}-{}",
982 std::process::id(),
983 std::time::SystemTime::now()
984 .duration_since(std::time::UNIX_EPOCH)
985 .unwrap()
986 .as_nanos()
987 ));
988 let metadata_path = write_recording(&root, "ri16_be", &[0, 1, 0, 2, 0, 3]);
989 let mut recording = Recording::open(&metadata_path).unwrap();
990 assert_eq!(recording.frame_count().unwrap(), 3);
991 assert_eq!(
992 recording.read_frames(1, 1).unwrap(),
993 vec![SampleValue::RealI16(2)]
994 );
995 std::fs::remove_dir_all(root).unwrap();
996 }
997
998 #[test]
999 fn recording_writer_publishes_dataset_then_matching_metadata() {
1000 let root = std::env::temp_dir().join(format!(
1001 "sigmf-recording-writer-test-{}-{}",
1002 std::process::id(),
1003 std::time::SystemTime::now()
1004 .duration_since(std::time::UNIX_EPOCH)
1005 .unwrap()
1006 .as_nanos()
1007 ));
1008 std::fs::create_dir_all(&root).unwrap();
1009 let metadata_path = root.join("written.sigmf-meta");
1010 let metadata = Metadata::new(
1011 Global::new("1.2.6", Datatype::from_str("ru8").unwrap()),
1012 Vec::new(),
1013 Vec::new(),
1014 );
1015 let mut writer = RecordingWriter::new(&metadata_path, metadata).unwrap();
1016 writer
1017 .write_frames(&[
1018 SampleValue::RealUnsignedBytes(9),
1019 SampleValue::RealUnsignedBytes(10),
1020 ])
1021 .unwrap();
1022 let digest = writer.finish().unwrap();
1023
1024 let metadata = Metadata::from_path(&metadata_path).unwrap();
1025 assert_eq!(metadata.global().sha512.as_deref(), Some(digest.as_str()));
1026 let mut recording = Recording::open(&metadata_path).unwrap();
1027 recording.verify_dataset_digest().unwrap();
1028 assert_eq!(
1029 recording.read_frames(0, 2).unwrap(),
1030 [
1031 SampleValue::RealUnsignedBytes(9),
1032 SampleValue::RealUnsignedBytes(10)
1033 ]
1034 );
1035 assert!(
1036 RecordingWriter::new(
1037 &metadata_path,
1038 Metadata::new(
1039 Global::new("1.2.6", Datatype::from_str("ru8").unwrap()),
1040 Vec::new(),
1041 Vec::new(),
1042 )
1043 )
1044 .is_err()
1045 );
1046 std::fs::remove_dir_all(root).unwrap();
1047 }
1048
1049 #[test]
1050 fn recording_writer_rolls_back_dataset_when_metadata_publication_fails() {
1051 let root = std::env::temp_dir().join(format!(
1052 "sigmf-recording-rollback-test-{}-{}",
1053 std::process::id(),
1054 std::time::SystemTime::now()
1055 .duration_since(std::time::UNIX_EPOCH)
1056 .unwrap()
1057 .as_nanos()
1058 ));
1059 std::fs::create_dir_all(&root).unwrap();
1060 let metadata_path = root.join("written.sigmf-meta");
1061 let dataset_path = root.join("written.sigmf-data");
1062 let metadata = Metadata::new(
1063 Global::new("1.2.6", Datatype::from_str("ru8").unwrap()),
1064 Vec::new(),
1065 Vec::new(),
1066 );
1067 let mut writer = RecordingWriter::new(&metadata_path, metadata).unwrap();
1068 writer
1069 .write_frames(&[SampleValue::RealUnsignedBytes(9)])
1070 .unwrap();
1071 std::fs::write(&metadata_path, b"existing metadata").unwrap();
1072
1073 assert!(writer.finish().is_err());
1074 assert_eq!(std::fs::read(&metadata_path).unwrap(), b"existing metadata");
1075 assert!(!dataset_path.exists());
1076 std::fs::remove_dir_all(root).unwrap();
1077 }
1078
1079 #[test]
1080 fn maps_absolute_sample_indices_through_core_offset() {
1081 let root = std::env::temp_dir().join(format!(
1082 "sigmf-offset-test-{}-{}",
1083 std::process::id(),
1084 std::time::SystemTime::now()
1085 .duration_since(std::time::UNIX_EPOCH)
1086 .unwrap()
1087 .as_nanos()
1088 ));
1089 let metadata_path = write_recording(&root, "ru8", &[10, 20, 30]);
1090 let mut metadata = Metadata::from_path(&metadata_path).unwrap();
1091 metadata.set_offset(1000);
1092 std::fs::write(&metadata_path, metadata.to_vec().unwrap()).unwrap();
1093
1094 let mut recording = Recording::open(&metadata_path).unwrap();
1095 assert_eq!(recording.first_sample_index(), 1000);
1096 assert_eq!(recording.frame_count().unwrap(), 3);
1097 assert_eq!(
1098 recording.read_frames(1001, 1).unwrap(),
1099 vec![SampleValue::RealUnsignedBytes(20)]
1100 );
1101 assert!(matches!(
1102 recording.read_frames(999, 1),
1103 Err(SigmfError::InvalidRange(_))
1104 ));
1105 assert!(matches!(
1106 recording.read_frames(1003, 1),
1107 Err(SigmfError::InvalidRange(_))
1108 ));
1109 std::fs::remove_dir_all(root).unwrap();
1110 }
1111
1112 #[test]
1113 fn frame_blocks_are_bounded_and_keep_errors_visible() {
1114 let root = std::env::temp_dir().join(format!(
1115 "sigmf-frame-blocks-test-{}-{}",
1116 std::process::id(),
1117 std::time::SystemTime::now()
1118 .duration_since(std::time::UNIX_EPOCH)
1119 .unwrap()
1120 .as_nanos()
1121 ));
1122 let metadata_path = write_recording(&root, "ru8", &[1, 2, 3, 4, 5]);
1123 let mut recording = Recording::open(&metadata_path).unwrap();
1124 let blocks: Vec<_> = recording.frame_blocks(0, 5, 2).unwrap().collect();
1125 assert_eq!(
1126 blocks
1127 .into_iter()
1128 .map(Result::unwrap)
1129 .map(|block| block.len())
1130 .collect::<Vec<_>>(),
1131 [2, 2, 1]
1132 );
1133 assert!(recording.frame_blocks(0, 1, 0).is_err());
1134 assert!(recording.frame_blocks(4, 2, 1).is_err());
1135 std::fs::remove_dir_all(root).unwrap();
1136 }
1137
1138 #[test]
1139 fn reads_samples_across_capture_headers() {
1140 let root = std::env::temp_dir().join(format!(
1141 "sigmf-capture-headers-test-{}-{}",
1142 std::process::id(),
1143 std::time::SystemTime::now()
1144 .duration_since(std::time::UNIX_EPOCH)
1145 .unwrap()
1146 .as_nanos()
1147 ));
1148 std::fs::create_dir_all(&root).unwrap();
1149 let metadata_path = root.join("header.sigmf-meta");
1150 let mut global = Global::new("1.2.6", Datatype::from_str("ru8").unwrap());
1151 global.set_offset(100);
1152 let mut first_capture = Capture::new(100);
1153 first_capture.set_header_bytes(1);
1154 let mut second_capture = Capture::new(102);
1155 second_capture.set_header_bytes(1);
1156 let metadata = Metadata::new(global, Vec::new(), vec![first_capture, second_capture]);
1157 std::fs::write(&metadata_path, metadata.to_vec().unwrap()).unwrap();
1158 std::fs::write(root.join("header.sigmf-data"), [0xaa, 1, 2, 0xbb, 3]).unwrap();
1159
1160 let mut recording = Recording::open(&metadata_path).unwrap();
1161 assert_eq!(recording.frame_count().unwrap(), 3);
1162 assert_eq!(
1163 recording.read_frames(100, 3).unwrap(),
1164 [
1165 SampleValue::RealUnsignedBytes(1),
1166 SampleValue::RealUnsignedBytes(2),
1167 SampleValue::RealUnsignedBytes(3)
1168 ]
1169 );
1170 std::fs::remove_dir_all(root).unwrap();
1171 }
1172
1173 #[test]
1174 fn trailer_bytes_are_excluded_from_frames_but_included_in_digest() {
1175 let root = std::env::temp_dir().join(format!(
1176 "sigmf-trailer-test-{}-{}",
1177 std::process::id(),
1178 std::time::SystemTime::now()
1179 .duration_since(std::time::UNIX_EPOCH)
1180 .unwrap()
1181 .as_nanos()
1182 ));
1183 let metadata_path = write_recording(&root, "ru8", &[1, 2, 3, 0xaa, 0xbb]);
1184 let mut metadata = Metadata::from_path(&metadata_path).unwrap();
1185 metadata.set_trailing_bytes(2);
1186 std::fs::write(&metadata_path, metadata.to_vec().unwrap()).unwrap();
1187
1188 let mut recording = Recording::open(&metadata_path).unwrap();
1189 assert_eq!(recording.frame_count().unwrap(), 3);
1190 assert_eq!(
1191 recording.read_frames(0, 3).unwrap(),
1192 [
1193 SampleValue::RealUnsignedBytes(1),
1194 SampleValue::RealUnsignedBytes(2),
1195 SampleValue::RealUnsignedBytes(3)
1196 ]
1197 );
1198 assert_eq!(
1199 recording.dataset_digest().unwrap(),
1200 crate::utils::hash::hash_from_slice(&[1, 2, 3, 0xaa, 0xbb]).unwrap()
1201 );
1202 std::fs::remove_dir_all(root).unwrap();
1203 }
1204
1205 #[test]
1206 fn computes_and_verifies_hash_over_exact_dataset_bytes() {
1207 let root = std::env::temp_dir().join(format!(
1208 "sigmf-dataset-hash-test-{}-{}",
1209 std::process::id(),
1210 std::time::SystemTime::now()
1211 .duration_since(std::time::UNIX_EPOCH)
1212 .unwrap()
1213 .as_nanos()
1214 ));
1215 let metadata_path = write_recording(&root, "ru8", &[1, 2, 3]);
1216 let mut metadata = Metadata::from_path(&metadata_path).unwrap();
1217 let digest = crate::utils::hash::hash_from_slice(&[1, 2, 3]).unwrap();
1218 metadata.set_sha512(digest.clone());
1219 std::fs::write(&metadata_path, metadata.to_vec().unwrap()).unwrap();
1220
1221 let mut recording = Recording::open(&metadata_path).unwrap();
1222 assert_eq!(recording.dataset_digest().unwrap(), digest);
1223 recording.verify_dataset_digest().unwrap();
1224 metadata.set_sha512("0".repeat(128));
1225 std::fs::write(&metadata_path, metadata.to_vec().unwrap()).unwrap();
1226 let mut recording = Recording::open(&metadata_path).unwrap();
1227 assert!(matches!(
1228 recording.verify_dataset_digest(),
1229 Err(SigmfError::HashMismatch(_))
1230 ));
1231 std::fs::remove_dir_all(root).unwrap();
1232 }
1233
1234 #[test]
1235 fn metadata_only_recording_reports_unavailable_samples() {
1236 let root = std::env::temp_dir().join(format!(
1237 "sigmf-metadata-only-test-{}-{}",
1238 std::process::id(),
1239 std::time::SystemTime::now()
1240 .duration_since(std::time::UNIX_EPOCH)
1241 .unwrap()
1242 .as_nanos()
1243 ));
1244 std::fs::create_dir_all(&root).unwrap();
1245 let path = root.join("meta.sigmf-meta");
1246 let mut global = Global::new("1.2.6", Datatype::from_str("ru8").unwrap());
1247 global.set_metadata_only(true);
1248 std::fs::write(
1249 &path,
1250 Metadata::new(global, Vec::new(), Vec::new())
1251 .to_vec()
1252 .unwrap(),
1253 )
1254 .unwrap();
1255 let mut recording = Recording::open(&path).unwrap();
1256 assert!(matches!(
1257 recording.frame_count(),
1258 Err(SigmfError::NoDataset)
1259 ));
1260 assert!(matches!(
1261 recording.read_frames(0, 1),
1262 Err(SigmfError::NoDataset)
1263 ));
1264 std::fs::remove_dir_all(root).unwrap();
1265 }
1266}