1use anyhow::{bail, Context, Result};
8use record_core::{
9 gap, parse_record_stream, payload_descriptor_count_from_metadata,
10 validate_payload_entries_metadata, validate_track_listing_metadata, RecordStreamMetadata,
11 ResolvedPayloadEntry, CONTAINER_ECDC, CONTAINER_EXTENSION, CONTAINER_MOSS_NANO,
12 PAYLOAD_CONTAINER_ECDC, PAYLOAD_CONTAINER_GAP, RECORD_STREAM_HEADER_LENGTH,
13 RECORD_STREAM_MAGIC,
14};
15use record_descriptor::{RecordDescriptor, SignedReleaseReference};
16use std::fmt::Write as _;
17
18#[derive(Debug, Clone, Default)]
19pub struct InspectionOptions<'a> {
20 pub png_name: Option<&'a str>,
22
23 pub bundle_metadata: Option<&'a encodec_rs::metadata::OnnxFrameBundleMetadata>,
26
27 pub manifest: Option<ExternalManifest<'a>>,
31
32 pub max_chunks: usize,
34
35 pub max_hex_bytes: usize,
37
38 pub verbose_payload_entries: bool,
40}
41
42impl<'a> InspectionOptions<'a> {
43 pub fn verbose_defaults() -> Self {
44 Self {
45 png_name: None,
46 bundle_metadata: None,
47 manifest: None,
48 max_chunks: 12,
49 max_hex_bytes: 256,
50 verbose_payload_entries: false,
51 }
52 }
53}
54
55#[derive(Debug, Clone, Copy)]
56pub struct ExternalManifest<'a> {
57 pub name: &'a str,
58 pub bytes: &'a [u8],
59}
60
61pub fn inspect_record_png(png: &[u8], options: &InspectionOptions<'_>) -> Result<String> {
63 let decoded =
64 record_decode::decode_record_png(png).context("record_decode::decode_record_png failed")?;
65
66 let mut out = String::new();
67
68 section(&mut out, "FILE");
69 writeln!(out, " name: {}", options.png_name.unwrap_or("<memory>"))?;
70 writeln!(out, " bytes: {}", png.len())?;
71
72 if let Some((width, height, bit_depth, color_type)) = png_ihdr(png) {
73 writeln!(
74 out,
75 " PNG: {width}x{height}, bit_depth={bit_depth}, color_type={color_type}"
76 )?;
77 } else {
78 writeln!(out, " PNG: <could not read IHDR>")?;
79 }
80
81 writeln!(out, " decoded record profile: {}", decoded.record_profile)?;
82
83 report_descriptor(
84 &mut out,
85 &decoded.descriptor,
86 options.manifest,
87 options.max_hex_bytes,
88 )?;
89
90 report_stream(
91 &mut out,
92 &decoded.chunk_stream.bytes,
93 decoded.chunk_stream.pixel_count,
94 &decoded.record_profile,
95 options,
96 )?;
97 report_signing_state(
98 &mut out,
99 &decoded.descriptor,
100 &decoded.chunk_stream.bytes,
101 options.manifest,
102 )?;
103 report_final_summary(
104 &mut out,
105 png,
106 &decoded.descriptor,
107 &decoded.chunk_stream.bytes,
108 &decoded.record_profile,
109 )?;
110
111 Ok(out)
112}
113
114pub fn report_descriptor(
116 out: &mut String,
117 descriptor: &RecordDescriptor,
118 manifest: Option<ExternalManifest<'_>>,
119 max_hex_bytes: usize,
120) -> Result<()> {
121 section(out, "BRD1 RECORD DESCRIPTOR");
122
123 writeln!(out, " version: {}", descriptor.version)?;
124 writeln!(
125 out,
126 " checksum protected: {}",
127 descriptor.checksum_protected
128 )?;
129 writeln!(out, " b_value: {}", descriptor.b_value())?;
130 writeln!(
131 out,
132 " stream byte length: {}",
133 descriptor.stream_byte_length.to_string()
134 )?;
135 writeln!(out, " record profile: {}", descriptor.record_profile)?;
136 writeln!(
137 out,
138 " payload encoding: {}",
139 descriptor.payload_encoding
140 )?;
141 writeln!(
142 out,
143 " title: {}",
144 format_optional_text(descriptor.title.as_deref())
145 )?;
146 writeln!(
147 out,
148 " artist: {}",
149 format_optional_text(descriptor.artist.as_deref())
150 )?;
151 writeln!(
152 out,
153 " release ID: {}",
154 descriptor
155 .release_id
156 .map(record_descriptor::release_id_to_text)
157 .unwrap_or_else(|| "absent".to_owned())
158 )?;
159 writeln!(
160 out,
161 " catalog number: {}",
162 format_optional_text(descriptor.catalog_number.as_deref())
163 )?;
164 writeln!(
165 out,
166 " label: {}",
167 format_optional_text(descriptor.label.as_deref())
168 )?;
169 writeln!(
170 out,
171 " artwork credit: {}",
172 format_optional_text(descriptor.artwork_credit.as_deref())
173 )?;
174 writeln!(
175 out,
176 " canonical URL: {}",
177 format_optional_text(descriptor.canonical_url.as_deref())
178 )?;
179 writeln!(
180 out,
181 " YL catalogue code: {}",
182 descriptor
183 .canonical_url
184 .as_deref()
185 .and_then(yl_catalogue_code_from_url)
186 .unwrap_or_else(|| "absent".to_owned())
187 )?;
188 writeln!(
189 out,
190 " created at: {}",
191 format_optional_number(descriptor.created_at)
192 )?;
193
194 match descriptor.bsc_pointer.as_deref() {
195 Some(pointer) => {
196 writeln!(out, " BSC pointer bytes: {}", pointer.len())?;
197 writeln!(out, "{}", indent(&hex_prefix(pointer, max_hex_bytes), 4))?;
198 }
199 None => writeln!(out, " BSC pointer: absent")?,
200 }
201
202 report_signed_release_reference(
203 out,
204 descriptor.signed_release_reference.as_ref(),
205 max_hex_bytes,
206 )?;
207
208 if let Some(manifest) = manifest {
209 report_external_manifest(
210 out,
211 manifest,
212 descriptor.signed_release_reference.as_ref(),
213 max_hex_bytes,
214 )?;
215 } else if descriptor.signed_release_reference.is_some() {
216 writeln!(
217 out,
218 " manifest body: not embedded; supply an external manifest or resolve BSC/registry data"
219 )?;
220 }
221
222 Ok(())
223}
224
225pub fn report_signed_release_reference(
226 out: &mut String,
227 reference: Option<&SignedReleaseReference>,
228 max_hex_bytes: usize,
229) -> Result<()> {
230 section(out, "SIGNED RELEASE REFERENCE");
231
232 let Some(reference) = reference else {
233 writeln!(out, " absent")?;
234 return Ok(());
235 };
236
237 reference.validate()?;
238
239 writeln!(out, " envelope version: {}", reference.version)?;
240 writeln!(
241 out,
242 " release commitment bytes: {}",
243 reference.release_commitment_sha256.len()
244 )?;
245 writeln!(
246 out,
247 " release commitment (hex): {}",
248 hex::encode(reference.release_commitment_sha256)
249 )?;
250
251 match printable_utf8(&reference.key_id) {
252 Some(text) => writeln!(out, " key ID (UTF-8): {text}")?,
253 None => writeln!(
254 out,
255 " key ID (hex): {}",
256 hex::encode(&reference.key_id)
257 )?,
258 }
259
260 writeln!(out, " key ID bytes: {}", reference.key_id.len())?;
261 writeln!(
262 out,
263 " signature bytes: {}",
264 reference.signature.len()
265 )?;
266 writeln!(out, " signature prefix:")?;
267 writeln!(
268 out,
269 "{}",
270 indent(&hex_prefix(&reference.signature, max_hex_bytes.min(64)), 4)
271 )?;
272
273 Ok(())
274}
275
276pub fn report_external_manifest(
282 out: &mut String,
283 manifest: ExternalManifest<'_>,
284 reference: Option<&SignedReleaseReference>,
285 max_hex_bytes: usize,
286) -> Result<()> {
287 section(out, "EXTERNAL RELEASE MANIFEST");
288
289 writeln!(out, " source: {}", manifest.name)?;
290 writeln!(out, " bytes: {}", manifest.bytes.len())?;
291
292 if let Some(reference) = reference {
293 writeln!(
294 out,
295 " expected release commitment (hex): {}",
296 hex::encode(reference.release_commitment_sha256)
297 )?;
298 writeln!(
299 out,
300 " hash comparison: not attempted; algorithm registry is intentionally unsettled"
301 )?;
302 } else {
303 writeln!(out, " expected hash: unavailable")?;
304 }
305
306 if let Ok(value) = serde_json::from_slice::<serde_json::Value>(manifest.bytes) {
307 writeln!(out, " display format: JSON")?;
308 report_json_structure(out, &value)?;
309 } else if let Some(text) = printable_utf8(manifest.bytes) {
310 writeln!(out, " display format: UTF-8 text")?;
311 writeln!(out, "{}", indent(text, 4))?;
312 } else {
313 writeln!(out, " display format: binary")?;
314 writeln!(
315 out,
316 "{}",
317 indent(&hex_prefix(manifest.bytes, max_hex_bytes), 4)
318 )?;
319 }
320
321 Ok(())
322}
323
324pub fn report_stream(
326 out: &mut String,
327 stream: &[u8],
328 extracted_groove_pixels: usize,
329 record_profile: &str,
330 options: &InspectionOptions<'_>,
331) -> Result<()> {
332 section(out, "BRS1 RECORD STREAM");
333
334 writeln!(
335 out,
336 " extracted groove pixels: {}",
337 extracted_groove_pixels
338 )?;
339 writeln!(out, " stream bytes: {}", stream.len())?;
340 writeln!(out, " stream magic: {:?}", ascii_magic(stream))?;
341 writeln!(
342 out,
343 " BRS1 magic valid: {}",
344 stream.get(..4) == Some(RECORD_STREAM_MAGIC.as_slice())
345 )?;
346
347 let header_end = record_core::record_stream_header_end(stream)?;
348 writeln!(out, " BRS1 header end: {header_end}")?;
349 writeln!(
350 out,
351 " binary metadata bytes: {}",
352 header_end.saturating_sub(RECORD_STREAM_HEADER_LENGTH)
353 )?;
354 writeln!(
355 out,
356 " chunk-section bytes: {}",
357 stream.len().saturating_sub(header_end)
358 )?;
359 writeln!(out, " stream prefix:")?;
360 writeln!(
361 out,
362 "{}",
363 indent(&hex_prefix(stream, options.max_hex_bytes), 4)
364 )?;
365
366 let parsed = parse_record_stream(stream)?;
367
368 report_stream_summary(out, &parsed)?;
369 report_metadata_summary(out, &parsed.metadata)?;
370 report_chunks(out, &parsed, options.max_chunks)?;
371 report_actual_programme_layout(
372 out,
373 &parsed,
374 record_profile,
375 options.verbose_payload_entries,
376 )?;
377 report_entries(out, &parsed, options)?;
378 report_spec_consistency(out, &parsed, record_profile)?;
379
380 Ok(())
381}
382
383fn report_stream_summary(out: &mut String, parsed: &record_core::RecordStream) -> Result<()> {
384 section(out, "BRS1 SUMMARY");
385
386 let musical_revolutions = parsed
387 .metadata
388 .tracks
389 .iter()
390 .map(|track| track.revolution_count)
391 .sum::<usize>();
392
393 let track_gap_entries = parsed
399 .metadata
400 .track_gaps
401 .iter()
402 .map(|gap| gap.revolution_count as usize)
403 .sum::<usize>();
404
405 let total_entries = parsed.metadata.payload_entries.len();
406
407 writeln!(out, " report mode: compact-v3")?;
408 writeln!(
409 out,
410 " tracks: {}",
411 parsed.metadata.tracks.len()
412 )?;
413 writeln!(
414 out,
415 " TrackGap ranges: {}",
416 parsed.metadata.track_gaps.len()
417 )?;
418 writeln!(out, " musical timeline entries: {musical_revolutions}")?;
419 writeln!(out, " TrackGap timeline entries: {track_gap_entries}")?;
420 writeln!(out, " total timeline entries: {total_entries}")?;
421 writeln!(out, " transport chunks: {}", parsed.chunks.len())?;
422
423 if musical_revolutions + track_gap_entries != total_entries {
424 bail!(
425 "musical timeline entries ({musical_revolutions}) + TrackGap timeline entries \
426 ({track_gap_entries}) != total timeline entries ({total_entries}); every payload \
427 entry must belong to exactly one track or track gap"
428 );
429 }
430
431 Ok(())
432}
433
434fn report_metadata_summary(out: &mut String, metadata: &RecordStreamMetadata) -> Result<()> {
435 section(out, "BRS1 HEADER METADATA");
436
437 let descriptor_count = payload_descriptor_count_from_metadata(metadata)?;
438
439 writeln!(out, " metadata version: {}", metadata.version)?;
440 writeln!(out, " encrypted: {}", metadata.encrypted)?;
441 writeln!(out, " payload descriptors: {descriptor_count}")?;
442 writeln!(
443 out,
444 " payload entries: {}",
445 metadata.payload_entries.len()
446 )?;
447 writeln!(
448 out,
449 " tracks: {}",
450 metadata.tracks.len()
451 )?;
452
453 section(out, "BRS1 PAYLOAD DESCRIPTORS");
454
455 for (index, descriptor) in metadata.payload_descriptors.iter().enumerate() {
456 writeln!(out, " descriptor[{index}]")?;
457 writeln!(out, " container: {}", descriptor.container)?;
458 writeln!(
459 out,
460 " codec: {}",
461 descriptor.codec.as_deref().unwrap_or("<absent>")
462 )?;
463 writeln!(
464 out,
465 " sample rate: {}",
466 descriptor
467 .sample_rate
468 .map(|value| value.to_string())
469 .unwrap_or_else(|| "<absent>".to_owned())
470 )?;
471 writeln!(
472 out,
473 " channels: {}",
474 descriptor
475 .channels
476 .map(|value| value.to_string())
477 .unwrap_or_else(|| "<absent>".to_owned())
478 )?;
479 writeln!(
480 out,
481 " block samples: {}",
482 descriptor
483 .block_samples
484 .map(|value| value.to_string())
485 .unwrap_or_else(|| "<absent>".to_owned())
486 )?;
487 writeln!(
488 out,
489 " output offset samples: {}",
490 descriptor
491 .output_offset_samples
492 .map(|value| value.to_string())
493 .unwrap_or_else(|| "<absent>".to_owned())
494 )?;
495 writeln!(
496 out,
497 " output samples: {}",
498 descriptor
499 .output_samples
500 .map(|value| value.to_string())
501 .unwrap_or_else(|| "<absent>".to_owned())
502 )?;
503
504 match descriptor.codec_metadata.as_deref() {
505 Some(bytes) => {
506 writeln!(out, " codec metadata bytes: {}", bytes.len())?;
507 match std::str::from_utf8(bytes) {
508 Ok(text) => {
509 writeln!(out, " codec metadata UTF-8: yes")?;
510 match serde_json::from_str::<serde_json::Value>(text) {
511 Ok(value) => {
512 writeln!(out, " codec metadata JSON: yes")?;
513 writeln!(
514 out,
515 " codec metadata value: {}",
516 serde_json::to_string(&value)?
517 )?;
518 }
519 Err(error) => {
520 writeln!(out, " codec metadata JSON: no")?;
521 writeln!(out, " codec metadata error: {error}")?;
522 writeln!(out, "{}", indent(&hex_prefix(bytes, 384), 6))?;
523 }
524 }
525 }
526 Err(error) => {
527 writeln!(out, " codec metadata UTF-8: no")?;
528 writeln!(out, " codec metadata error: {error}")?;
529 writeln!(out, "{}", indent(&hex_prefix(bytes, 384), 6))?;
530 }
531 }
532 }
533 None => {
534 writeln!(out, " codec metadata: absent")?;
535 }
536 }
537 }
538
539 section(out, "BRS1 PAYLOAD ENTRY TABLE");
540
541 let display_indices = track_boundary_entry_indices(metadata);
542 let mut byte_offset = 0usize;
543 let mut previous_printed_index = None;
544
545 for (index, entry) in metadata.payload_entries.iter().enumerate() {
546 if display_indices.binary_search(&index).is_ok() {
547 if let Some(previous_index) = previous_printed_index {
548 let omitted = index.saturating_sub(previous_index + 1);
549 if omitted > 0 {
550 writeln!(out, " ... {omitted} intermediate payload entries omitted")?;
551 }
552 }
553
554 writeln!(
555 out,
556 " entry[{index}]: byte_offset={} byte_length={} descriptor_index={}",
557 byte_offset, entry.byte_length, entry.payload_descriptor_index
558 )?;
559 previous_printed_index = Some(index);
560 }
561
562 byte_offset = byte_offset
563 .checked_add(entry.byte_length)
564 .context("payload entry byte offset overflow while reporting header table")?;
565 }
566
567 section(out, "BRS1 TRACK TABLE");
568
569 for (index, track) in metadata.tracks.iter().enumerate() {
570 writeln!(
571 out,
572 " track[{index}]: title={:?} first_revolution_index={} revolution_count={}",
573 track.title, track.first_revolution_index, track.revolution_count
574 )?;
575 }
576
577 Ok(())
578}
579
580fn track_boundary_entry_indices(metadata: &RecordStreamMetadata) -> Vec<usize> {
581 let mut indices = Vec::new();
582
583 for track in &metadata.tracks {
584 if track.revolution_count == 0 {
585 continue;
586 }
587
588 let first = track.first_revolution_index;
589 let last = first.saturating_add(track.revolution_count.saturating_sub(1));
590
591 if first < metadata.payload_entries.len() {
592 indices.push(first);
593 }
594 if last < metadata.payload_entries.len() && last != first {
595 indices.push(last);
596 }
597 }
598
599 indices.sort_unstable();
600 indices.dedup();
601 indices
602}
603
604fn report_chunks(
605 out: &mut String,
606 parsed: &record_core::RecordStream,
607 max_chunks: usize,
608) -> Result<()> {
609 section(out, "BRS1 TRANSPORT CHUNKS");
610
611 writeln!(out, " chunk count: {}", parsed.chunks.len())?;
612
613 for (index, chunk) in parsed.chunks.iter().enumerate().take(max_chunks) {
614 writeln!(
615 out,
616 " chunk[{index}]: payload_bytes={} crc32={:08x} nonce={}",
617 chunk.payload.len(),
618 chunk.crc32,
619 if chunk.nonce.is_some() {
620 "present"
621 } else {
622 "absent"
623 }
624 )?;
625 }
626
627 if parsed.chunks.len() > max_chunks {
628 writeln!(
629 out,
630 " ... {} more chunks",
631 parsed.chunks.len() - max_chunks
632 )?;
633 }
634
635 Ok(())
636}
637
638#[derive(Debug, Clone)]
639struct SpecCheck {
640 passed: bool,
641 label: String,
642 detail: String,
643}
644
645impl SpecCheck {
646 fn pass(label: impl Into<String>, detail: impl Into<String>) -> Self {
647 Self {
648 passed: true,
649 label: label.into(),
650 detail: detail.into(),
651 }
652 }
653
654 fn fail(label: impl Into<String>, detail: impl Into<String>) -> Self {
655 Self {
656 passed: false,
657 label: label.into(),
658 detail: detail.into(),
659 }
660 }
661}
662
663fn report_actual_programme_layout(
664 out: &mut String,
665 parsed: &record_core::RecordStream,
666 record_profile: &str,
667 _verbose_payload_entries: bool,
668) -> Result<()> {
669 section(out, "DERIVED PROGRAMME LAYOUT");
670
671 let payload = record_core::record_stream_payload_bytes(parsed);
672 let entries = validate_payload_entries_metadata(&parsed.metadata, Some(payload.len()))?;
673
674 let mut track_by_entry: Vec<Option<(usize, &str)>> =
675 vec![None; parsed.metadata.payload_entries.len()];
676
677 for (track_index, track) in parsed.metadata.tracks.iter().enumerate() {
678 let end = track
679 .first_revolution_index
680 .checked_add(track.revolution_count)
681 .context("track revolution range overflow while reporting layout")?;
682
683 for entry_index in track.first_revolution_index..end {
684 if let Some(slot) = track_by_entry.get_mut(entry_index) {
685 *slot = Some((track_index + 1, track.title.as_str()));
686 }
687 }
688 }
689
690 let mut chunk_ranges = Vec::with_capacity(parsed.chunks.len());
691 let mut chunk_offset = 0usize;
692 for (chunk_index, chunk) in parsed.chunks.iter().enumerate() {
693 let end = chunk_offset
694 .checked_add(chunk.payload.len())
695 .context("transport chunk byte range overflow")?;
696 chunk_ranges.push((chunk_index, chunk_offset, end));
697 chunk_offset = end;
698 }
699
700 let sample_rate = parsed
701 .metadata
702 .payload_descriptors
703 .iter()
704 .find_map(|descriptor| descriptor.sample_rate);
705
706 let display_indices = track_boundary_entry_indices(&parsed.metadata);
707 let mut sample_cursor = 0u64;
708 let mut sample_cursor_known = true;
709 let mut previous_printed_index = None;
710
711 writeln!(
712 out,
713 " note: derived from header tables and payload bodies"
714 )?;
715 writeln!(out, " record profile: {record_profile}")?;
716 writeln!(out, " logical payload entries: {}", entries.len())?;
717 writeln!(out, " transport chunks: {}", parsed.chunks.len())?;
718 writeln!(out, " stored payload bytes: {}", payload.len())?;
719 writeln!(
720 out,
721 " sample rate: {}",
722 sample_rate
723 .map(|value| value.to_string())
724 .unwrap_or_else(|| "<absent>".to_owned())
725 )?;
726
727 for entry in &entries {
728 let descriptor = parsed
729 .metadata
730 .payload_descriptors
731 .get(entry.payload_descriptor_index as usize)
732 .context("payload descriptor index is out of range")?;
733
734 let entry_end = entry
735 .byte_offset
736 .checked_add(entry.byte_length)
737 .context("payload entry byte range overflow")?;
738
739 let entry_bytes = payload
740 .get(entry.byte_offset..entry_end)
741 .context("payload entry exceeds stored payload")?;
742
743 let ownership = track_by_entry[entry.index]
744 .map(|(number, title)| format!("track {number} {:?}", title));
745
746 let should_print = display_indices.binary_search(&entry.index).is_ok();
747
748 let sample_count_result: Result<(u64, &'static str)> = if descriptor
749 .container
750 .eq_ignore_ascii_case(PAYLOAD_CONTAINER_GAP)
751 {
752 gap::decode_gap_header(entry_bytes)
753 .map(|header| (header.sample_count, "GAP1 header"))
754 .context("failed to read GAP1 sample count")
755 } else if descriptor
756 .container
757 .eq_ignore_ascii_case(PAYLOAD_CONTAINER_ECDC)
758 {
759 record_core::ecdc::headerless_entry_sample_count(entry_bytes, descriptor)
760 .map(|samples| (samples, "headerless ECDC entry"))
761 .context("failed to read exact headerless ECDC sample count")
762 } else {
763 descriptor
764 .output_samples
765 .map(|samples| (u64::from(samples), "descriptor outputSamples"))
766 .context("no exact sample-count rule for this payload entry")
767 };
768
769 let covering_chunks = chunk_ranges
770 .iter()
771 .filter_map(|(chunk_index, chunk_start, chunk_end)| {
772 let overlap_start = entry.byte_offset.max(*chunk_start);
773 let overlap_end = entry_end.min(*chunk_end);
774
775 if overlap_start < overlap_end {
776 Some(format!(
777 "{chunk_index}[{}..{}]",
778 overlap_start - *chunk_start,
779 overlap_end - *chunk_start
780 ))
781 } else {
782 None
783 }
784 })
785 .collect::<Vec<_>>();
786
787 if should_print {
788 if let Some(previous_index) = previous_printed_index {
789 let omitted = entry.index.saturating_sub(previous_index + 1);
790 if omitted > 0 {
791 writeln!(out, " ... {omitted} intermediate payload entries omitted")?;
792 }
793 }
794
795 writeln!(
796 out,
797 " entry[{}] {}",
798 entry.index,
799 ownership.as_deref().unwrap_or("semantic gap")
800 )?;
801 writeln!(
802 out,
803 " descriptor index: {}",
804 entry.payload_descriptor_index
805 )?;
806 writeln!(
807 out,
808 " container / codec: {} / {}",
809 descriptor.container,
810 descriptor.codec.as_deref().unwrap_or("<absent>")
811 )?;
812 writeln!(
813 out,
814 " stored byte range: {}..{} ({} bytes)",
815 entry.byte_offset, entry_end, entry.byte_length
816 )?;
817
818 if descriptor
819 .container
820 .eq_ignore_ascii_case(PAYLOAD_CONTAINER_ECDC)
821 {
822 writeln!(out, " payload prefix: headerless codec body")?;
823 } else {
824 writeln!(
825 out,
826 " payload magic: {:?}",
827 ascii_magic(entry_bytes)
828 )?;
829 }
830
831 writeln!(
832 out,
833 " transport coverage: {}",
834 if covering_chunks.is_empty() {
835 "<none>".to_owned()
836 } else {
837 covering_chunks.join(", ")
838 }
839 )?;
840 previous_printed_index = Some(entry.index);
841 }
842
843 match sample_count_result {
844 Ok((0, source))
845 if descriptor
846 .container
847 .eq_ignore_ascii_case(PAYLOAD_CONTAINER_ECDC) =>
848 {
849 sample_cursor_known = false;
850 if should_print {
851 writeln!(out, " sample count: invalid zero ({source})")?;
852 writeln!(
853 out,
854 " programme samples: unavailable from this entry onward"
855 )?;
856 }
857 }
858 Ok((samples, source)) => {
859 let start_sample = sample_cursor;
860 let end_sample = start_sample
861 .checked_add(samples)
862 .context("programme sample range overflow")?;
863
864 if should_print {
865 writeln!(out, " sample count: {samples} ({source})")?;
866 if sample_cursor_known {
867 writeln!(
868 out,
869 " programme samples: {start_sample}..{end_sample}"
870 )?;
871 if let Some(rate) = sample_rate.filter(|value| *value > 0) {
872 writeln!(
873 out,
874 " programme time: {:.6}..{:.6} s",
875 start_sample as f64 / rate as f64,
876 end_sample as f64 / rate as f64
877 )?;
878 }
879 } else {
880 writeln!(
881 out,
882 " programme samples: unknown because an earlier entry could not be measured"
883 )?;
884 }
885 }
886
887 sample_cursor = end_sample;
888 }
889 Err(error) => {
890 sample_cursor_known = false;
891 if should_print {
892 writeln!(out, " sample count: unavailable")?;
893 writeln!(out, " sample-count error: {error:#}")?;
894 }
895 }
896 }
897 }
898
899 if sample_cursor_known {
900 writeln!(out, " total programme samples: {sample_cursor}")?;
901 if let Some(rate) = sample_rate.filter(|value| *value > 0) {
902 writeln!(
903 out,
904 " total programme duration: {:.6} s",
905 sample_cursor as f64 / rate as f64
906 )?;
907 }
908 } else {
909 writeln!(
910 out,
911 " total programme samples: unavailable because one or more entries could not be measured"
912 )?;
913 }
914
915 Ok(())
916}
917
918fn collect_spec_checks(parsed: &record_core::RecordStream, record_profile: &str) -> Vec<SpecCheck> {
919 let mut checks = Vec::new();
920
921 checks.push(
922 if parsed.metadata.version == record_core::RECORD_STREAM_METADATA_VERSION {
923 SpecCheck::pass(
924 "BRS1 metadata version",
925 format!("version {}", parsed.metadata.version),
926 )
927 } else {
928 SpecCheck::fail(
929 "BRS1 metadata version",
930 format!(
931 "expected {}, found {}",
932 record_core::RECORD_STREAM_METADATA_VERSION,
933 parsed.metadata.version
934 ),
935 )
936 },
937 );
938
939 checks.push(
940 match record_core::normalize_record_profile_name(record_profile) {
941 Ok(profile) => SpecCheck::pass("Record profile", profile),
942 Err(error) => SpecCheck::fail("Record profile", format!("{error:#}")),
943 },
944 );
945
946 checks.push(match validate_track_listing_metadata(&parsed.metadata) {
947 Ok(()) => SpecCheck::pass(
948 "Musical track ranges",
949 "all payload entries are covered exactly once by either Track or TrackGap",
950 ),
951 Err(error) => SpecCheck::fail("Musical track ranges", format!("{error:#}")),
952 });
953
954 let payload = record_core::record_stream_payload_bytes(parsed);
955 let resolved = match validate_payload_entries_metadata(&parsed.metadata, Some(payload.len())) {
956 Ok(entries) => {
957 checks.push(SpecCheck::pass(
958 "Payload entry byte coverage",
959 format!(
960 "{} entries cover all {} stored payload bytes",
961 entries.len(),
962 payload.len()
963 ),
964 ));
965 Some(entries)
966 }
967 Err(error) => {
968 checks.push(SpecCheck::fail(
969 "Payload entry byte coverage",
970 format!("{error:#}"),
971 ));
972 None
973 }
974 };
975
976 for (index, descriptor) in parsed.metadata.payload_descriptors.iter().enumerate() {
977 checks.push(match record_core::validate_payload_descriptor(descriptor) {
978 Ok(()) => SpecCheck::pass(
979 format!("Descriptor {index} generic validation"),
980 format!("container {}", descriptor.container),
981 ),
982 Err(error) => SpecCheck::fail(
983 format!("Descriptor {index} generic validation"),
984 format!("{error:#}"),
985 ),
986 });
987
988 if descriptor
989 .container
990 .eq_ignore_ascii_case(PAYLOAD_CONTAINER_ECDC)
991 {
992 let mut problems = Vec::new();
993
994 if !matches!(
995 descriptor.codec.as_deref(),
996 Some(codec) if codec.eq_ignore_ascii_case(PAYLOAD_CONTAINER_ECDC)
997 ) {
998 problems.push("codec is absent or not ECDC".to_owned());
999 }
1000 if !matches!(descriptor.sample_rate, Some(value) if value > 0) {
1001 problems.push("sampleRate is absent or zero".to_owned());
1002 }
1003 if !matches!(descriptor.channels, Some(value) if value > 0) {
1004 problems.push("channels is absent or zero".to_owned());
1005 }
1006 if descriptor.block_samples.is_none()
1007 || descriptor.output_offset_samples.is_none()
1008 || descriptor.output_samples.is_none()
1009 {
1010 problems.push("typed output geometry is incomplete".to_owned());
1011 }
1012
1013 match descriptor.codec_metadata.as_deref() {
1014 None => problems.push("codecMetadata is absent".to_owned()),
1015 Some(bytes) if bytes.is_empty() => {
1016 problems.push("codecMetadata is empty".to_owned())
1017 }
1018 Some(bytes) => {
1019 if let Err(error) = serde_json::from_slice::<serde_json::Value>(bytes) {
1020 problems.push(format!("codecMetadata is not valid JSON: {error}"));
1021 }
1022 }
1023 }
1024
1025 checks.push(if problems.is_empty() {
1026 SpecCheck::pass(
1027 format!("Descriptor {index} canonical ECDC shape"),
1028 "codec, sample format, output geometry and codecMetadata are present",
1029 )
1030 } else {
1031 SpecCheck::fail(
1032 format!("Descriptor {index} canonical ECDC shape"),
1033 problems.join("; "),
1034 )
1035 });
1036 }
1037
1038 if descriptor
1039 .container
1040 .eq_ignore_ascii_case(PAYLOAD_CONTAINER_GAP)
1041 {
1042 let mut problems = Vec::new();
1043
1044 if !matches!(
1045 descriptor.codec.as_deref(),
1046 Some(codec) if codec.eq_ignore_ascii_case("GAP")
1047 ) {
1048 problems.push("codec is absent or not GAP".to_owned());
1049 }
1050 if !matches!(descriptor.sample_rate, Some(value) if value > 0) {
1051 problems.push("sampleRate is absent or zero".to_owned());
1052 }
1053 if !matches!(descriptor.channels, Some(value) if value > 0) {
1054 problems.push("channels is absent or zero".to_owned());
1055 }
1056 if descriptor.block_samples.is_some()
1057 || descriptor.output_offset_samples.is_some()
1058 || descriptor.output_samples.is_some()
1059 {
1060 problems.push("GAP descriptor incorrectly contains output geometry".to_owned());
1061 }
1062 if descriptor.codec_metadata.is_some() {
1063 problems.push("GAP descriptor incorrectly contains codecMetadata".to_owned());
1064 }
1065
1066 checks.push(if problems.is_empty() {
1067 SpecCheck::pass(
1068 format!("Descriptor {index} canonical GAP shape"),
1069 "container GAP, codec GAP, sample rate/channels present, no geometry",
1070 )
1071 } else {
1072 SpecCheck::fail(
1073 format!("Descriptor {index} canonical GAP shape"),
1074 problems.join("; "),
1075 )
1076 });
1077 }
1078 }
1079
1080 if let Some(entries) = resolved {
1081 for entry in entries {
1082 let descriptor = match parsed
1083 .metadata
1084 .payload_descriptors
1085 .get(entry.payload_descriptor_index as usize)
1086 {
1087 Some(descriptor) => descriptor,
1088 None => {
1089 checks.push(SpecCheck::fail(
1090 format!("Entry {} descriptor reference", entry.index),
1091 "descriptor index is out of range",
1092 ));
1093 continue;
1094 }
1095 };
1096
1097 let end = match entry.byte_offset.checked_add(entry.byte_length) {
1098 Some(end) => end,
1099 None => {
1100 checks.push(SpecCheck::fail(
1101 format!("Entry {} byte range", entry.index),
1102 "byte range overflow",
1103 ));
1104 continue;
1105 }
1106 };
1107
1108 let bytes = match payload.get(entry.byte_offset..end) {
1109 Some(bytes) => bytes,
1110 None => {
1111 checks.push(SpecCheck::fail(
1112 format!("Entry {} byte range", entry.index),
1113 "entry exceeds stored payload",
1114 ));
1115 continue;
1116 }
1117 };
1118
1119 if descriptor
1120 .container
1121 .eq_ignore_ascii_case(PAYLOAD_CONTAINER_GAP)
1122 {
1123 checks.push(match gap::validate_gap_payload(bytes) {
1124 Ok(header) => SpecCheck::pass(
1125 format!("Entry {} GAP1 payload", entry.index),
1126 format!(
1127 "{} samples, {} bytes, seed 0x{:08x}",
1128 header.sample_count, header.payload_byte_length, header.seed
1129 ),
1130 ),
1131 Err(error) => SpecCheck::fail(
1132 format!("Entry {} GAP1 payload", entry.index),
1133 format!("{error:#}"),
1134 ),
1135 });
1136 }
1137
1138 if descriptor
1139 .container
1140 .eq_ignore_ascii_case(PAYLOAD_CONTAINER_ECDC)
1141 {
1142 checks.push(
1143 match record_core::ecdc::headerless_entry_sample_count(bytes, descriptor) {
1144 Ok(0) => SpecCheck::fail(
1145 format!("Entry {} exact ECDC sample count", entry.index),
1146 "musical ECDC entry resolved to zero samples",
1147 ),
1148 Ok(samples) => SpecCheck::pass(
1149 format!("Entry {} exact ECDC sample count", entry.index),
1150 format!("{samples} samples"),
1151 ),
1152 Err(error) => SpecCheck::fail(
1153 format!("Entry {} exact ECDC sample count", entry.index),
1154 format!("{error:#}"),
1155 ),
1156 },
1157 );
1158 }
1159 }
1160 }
1161
1162 checks.push(
1163 match record_core::build_programme_map(parsed, Some(record_profile)) {
1164 Ok(map) => SpecCheck::pass(
1165 "Pre-decode programme map",
1166 format!(
1167 "{} samples across {} regions",
1168 map.total_samples,
1169 map.regions.len()
1170 ),
1171 ),
1172 Err(error) => SpecCheck::fail("Pre-decode programme map", format!("{error:#}")),
1173 },
1174 );
1175
1176 checks
1177}
1178
1179fn report_spec_consistency(
1180 out: &mut String,
1181 parsed: &record_core::RecordStream,
1182 record_profile: &str,
1183) -> Result<()> {
1184 section(out, "CHECK RESULTS");
1185
1186 let checks = collect_spec_checks(parsed, record_profile);
1187 let mut omitted_entry_passes = 0usize;
1188
1189 for check in &checks {
1190 let repetitive_entry_pass = check.passed
1191 && check.label.starts_with("Entry ")
1192 && (check.label.ends_with(" exact ECDC sample count")
1193 || check.label.ends_with(" GAP1 payload"));
1194
1195 if repetitive_entry_pass {
1196 omitted_entry_passes += 1;
1197 continue;
1198 }
1199
1200 writeln!(out, " {} {}", status_mark(check.passed), check.label)?;
1201 writeln!(out, " {}", check.detail)?;
1202 }
1203
1204 if omitted_entry_passes > 0 {
1205 writeln!(
1206 out,
1207 " {} {omitted_entry_passes} repetitive per-entry checks passed and were omitted",
1208 green_tick()
1209 )?;
1210 }
1211
1212 Ok(())
1213}
1214
1215#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1216enum SigningCoverage {
1217 IndirectlySignedUnchecked,
1218 Unsigned,
1219}
1220
1221impl SigningCoverage {
1222 fn as_str(self) -> &'static str {
1223 match self {
1224 Self::IndirectlySignedUnchecked => "signed via release commitment (unchecked)",
1225 Self::Unsigned => "unsigned",
1226 }
1227 }
1228}
1229
1230fn report_signing_state(
1231 out: &mut String,
1232 descriptor: &RecordDescriptor,
1233 stream: &[u8],
1234 manifest: Option<ExternalManifest<'_>>,
1235) -> Result<()> {
1236 section(out, "SIGNING STATE");
1237 let has_signed_reference = descriptor.signed_release_reference.is_some();
1238 let has_canonical_url = descriptor.canonical_url.is_some();
1239 let yl_issuance_state = if has_signed_reference && has_canonical_url {
1240 "final YL issuance markers present"
1241 } else if has_signed_reference || has_canonical_url {
1242 "partial YL issuance markers present"
1243 } else {
1244 "no YL issuance markers present"
1245 };
1246
1247 let signed_reference_state = match descriptor.signed_release_reference.as_ref() {
1248 None => "absent".to_owned(),
1249 Some(reference) => match reference.validate() {
1250 Ok(()) => {
1251 "present; structurally valid; cryptographic verification not checked".to_owned()
1252 }
1253 Err(error) => format!("present; structurally invalid ({error:#})"),
1254 },
1255 };
1256
1257 writeln!(out, " signed release reference: {signed_reference_state}")?;
1258 writeln!(out, " YL issuance state: {yl_issuance_state}")?;
1259 writeln!(
1260 out,
1261 " release commitment hash: {}",
1262 if has_signed_reference {
1263 "present in signed reference"
1264 } else {
1265 "absent"
1266 }
1267 )?;
1268 writeln!(
1269 out,
1270 " external manifest: {}",
1271 if manifest.is_some() {
1272 "present; displayed for inspection only"
1273 } else {
1274 "absent"
1275 }
1276 )?;
1277 writeln!(
1278 out,
1279 " BRS1 stream bytes: {}",
1280 if has_signed_reference {
1281 format!(
1282 "{}; structural parsing passed ({} bytes)",
1283 SigningCoverage::IndirectlySignedUnchecked.as_str(),
1284 stream.len()
1285 )
1286 } else {
1287 format!(
1288 "{}; structural parsing passed ({} bytes)",
1289 SigningCoverage::Unsigned.as_str(),
1290 stream.len()
1291 )
1292 }
1293 )?;
1294
1295 report_field_signing_state(
1296 out,
1297 "record_profile",
1298 descriptor.record_profile.as_str(),
1299 if has_signed_reference {
1300 SigningCoverage::IndirectlySignedUnchecked
1301 } else {
1302 SigningCoverage::Unsigned
1303 },
1304 if has_signed_reference {
1305 "valid canonical profile code path; would be covered indirectly by the signed release commitment"
1306 } else {
1307 "valid canonical profile code path; not signed in this file"
1308 },
1309 )?;
1310 report_field_signing_state(
1311 out,
1312 "payload_encoding",
1313 descriptor.payload_encoding.as_str(),
1314 if has_signed_reference {
1315 SigningCoverage::IndirectlySignedUnchecked
1316 } else {
1317 SigningCoverage::Unsigned
1318 },
1319 if has_signed_reference {
1320 "valid canonical payload-encoding code path; would be covered indirectly by the signed release commitment"
1321 } else {
1322 "valid canonical payload-encoding code path; not signed in this file"
1323 },
1324 )?;
1325 report_optional_text_field_signing_state(
1326 out,
1327 "title",
1328 descriptor.title.as_deref(),
1329 SigningCoverage::Unsigned,
1330 "decoded and UTF-8 validated",
1331 )?;
1332 report_optional_text_field_signing_state(
1333 out,
1334 "artist",
1335 descriptor.artist.as_deref(),
1336 SigningCoverage::Unsigned,
1337 "decoded and UTF-8 validated",
1338 )?;
1339 report_release_id_signing_state(out, descriptor.release_id, has_signed_reference)?;
1340 report_optional_text_field_signing_state(
1341 out,
1342 "catalog_number",
1343 descriptor.catalog_number.as_deref(),
1344 SigningCoverage::Unsigned,
1345 "decoded and UTF-8 validated",
1346 )?;
1347 report_optional_text_field_signing_state(
1348 out,
1349 "label",
1350 descriptor.label.as_deref(),
1351 SigningCoverage::Unsigned,
1352 "decoded and UTF-8 validated",
1353 )?;
1354 report_optional_text_field_signing_state(
1355 out,
1356 "artwork_credit",
1357 descriptor.artwork_credit.as_deref(),
1358 SigningCoverage::Unsigned,
1359 "decoded and UTF-8 validated",
1360 )?;
1361 report_optional_text_field_signing_state(
1362 out,
1363 "canonical_url",
1364 descriptor.canonical_url.as_deref(),
1365 SigningCoverage::Unsigned,
1366 "decoded and UTF-8 validated",
1367 )?;
1368 report_optional_number_field_signing_state(
1369 out,
1370 "created_at",
1371 descriptor.created_at,
1372 SigningCoverage::Unsigned,
1373 "decoded as descriptor metadata",
1374 )?;
1375 report_bytes_field_signing_state(
1376 out,
1377 "bsc_pointer",
1378 descriptor.bsc_pointer.as_deref(),
1379 SigningCoverage::Unsigned,
1380 "opaque bytes only; not validated by test-spin",
1381 )?;
1382
1383 Ok(())
1384}
1385
1386fn report_field_signing_state(
1387 out: &mut String,
1388 label: &str,
1389 value: &str,
1390 coverage: SigningCoverage,
1391 validity: &str,
1392) -> Result<()> {
1393 writeln!(
1394 out,
1395 " {label}: {} | {} | value={value:?}",
1396 coverage.as_str(),
1397 validity
1398 )?;
1399 Ok(())
1400}
1401
1402fn report_optional_text_field_signing_state(
1403 out: &mut String,
1404 label: &str,
1405 value: Option<&str>,
1406 coverage: SigningCoverage,
1407 validity: &str,
1408) -> Result<()> {
1409 let presence = match value {
1410 Some(value) => format!("present | value={value:?}"),
1411 None => "absent".to_owned(),
1412 };
1413 writeln!(
1414 out,
1415 " {label}: {} | {} | {presence}",
1416 coverage.as_str(),
1417 validity
1418 )?;
1419 Ok(())
1420}
1421
1422fn report_optional_number_field_signing_state(
1423 out: &mut String,
1424 label: &str,
1425 value: Option<u64>,
1426 coverage: SigningCoverage,
1427 validity: &str,
1428) -> Result<()> {
1429 let presence = match value {
1430 Some(value) => format!("present | value={value}"),
1431 None => "absent".to_owned(),
1432 };
1433 writeln!(
1434 out,
1435 " {label}: {} | {} | {presence}",
1436 coverage.as_str(),
1437 validity
1438 )?;
1439 Ok(())
1440}
1441
1442fn report_bytes_field_signing_state(
1443 out: &mut String,
1444 label: &str,
1445 value: Option<&[u8]>,
1446 coverage: SigningCoverage,
1447 validity: &str,
1448) -> Result<()> {
1449 let presence = match value {
1450 Some(value) => format!("present | {} bytes", value.len()),
1451 None => "absent".to_owned(),
1452 };
1453 writeln!(
1454 out,
1455 " {label}: {} | {} | {presence}",
1456 coverage.as_str(),
1457 validity
1458 )?;
1459 Ok(())
1460}
1461
1462fn report_release_id_signing_state(
1463 out: &mut String,
1464 value: Option<[u8; record_descriptor::RELEASE_ID_LENGTH]>,
1465 has_signed_reference: bool,
1466) -> Result<()> {
1467 let presence = match value {
1468 Some(bytes) => format!(
1469 "present | value={}",
1470 record_descriptor::release_id_to_text(bytes)
1471 ),
1472 None => "absent".to_owned(),
1473 };
1474 writeln!(
1475 out,
1476 " release_id: {} | {} | {presence}",
1477 if has_signed_reference {
1478 SigningCoverage::IndirectlySignedUnchecked.as_str()
1479 } else {
1480 SigningCoverage::Unsigned.as_str()
1481 },
1482 if has_signed_reference {
1483 "canonical raw 16-byte BRD1 field; would be covered indirectly by the signed release commitment"
1484 } else {
1485 "canonical raw 16-byte BRD1 field; not signed in this file"
1486 },
1487 )?;
1488 Ok(())
1489}
1490
1491fn report_entries(
1492 out: &mut String,
1493 parsed: &record_core::RecordStream,
1494 options: &InspectionOptions<'_>,
1495) -> Result<()> {
1496 section(out, "BRS1 PAYLOAD BODIES");
1497
1498 let payload = record_core::chunk_stream_payload_bytes(parsed);
1499 let entries = validate_payload_entries_metadata(&parsed.metadata, Some(payload.len()))?;
1500
1501 writeln!(out, " reconstructed payload bytes: {}", payload.len())?;
1502 writeln!(out, " logical payload entries: {}", entries.len())?;
1503
1504 let display_entries = track_boundary_entry_indices(&parsed.metadata);
1505
1506 let mut previous_printed_index = None;
1507
1508 for entry_index in display_entries {
1509 if let Some(previous_index) = previous_printed_index {
1510 let omitted = entry_index.saturating_sub(previous_index + 1);
1511 if omitted > 0 {
1512 writeln!(
1513 out,
1514 " ... {omitted} intermediate payload entries omitted; use --verbose to show all"
1515 )?;
1516 }
1517 }
1518
1519 let entry = &entries[entry_index];
1520 let bytes = payload_entry_bytes(&payload, entry)?;
1521
1522 section(
1523 out,
1524 &format!("PAYLOAD BODY {} / {}", entry.index + 1, entries.len()),
1525 );
1526
1527 writeln!(out, " entry index: {}", entry.index)?;
1528 writeln!(out, " byte offset: {}", entry.byte_offset)?;
1529 writeln!(out, " byte length: {}", entry.byte_length)?;
1530 writeln!(
1531 out,
1532 " descriptor index: {}",
1533 entry.payload_descriptor_index
1534 )?;
1535
1536 let descriptor = parsed
1537 .metadata
1538 .payload_descriptors
1539 .get(entry.payload_descriptor_index as usize)
1540 .context("payload descriptor index is out of range")?;
1541
1542 if descriptor
1543 .container
1544 .eq_ignore_ascii_case(PAYLOAD_CONTAINER_ECDC)
1545 {
1546 writeln!(out, " payload prefix: headerless codec body")?;
1547 } else {
1548 writeln!(out, " first four bytes: {:?}", ascii_magic(bytes))?;
1549 }
1550
1551 if bytes.get(..4) == Some(gap::GAP_MAGIC.as_slice()) {
1552 report_gap_payload_body(out, bytes)?;
1553 } else {
1554 writeln!(out, " body prefix:")?;
1555 writeln!(
1556 out,
1557 "{}",
1558 indent(&hex_prefix(bytes, options.max_hex_bytes.max(384)), 4)
1559 )?;
1560 }
1561
1562 previous_printed_index = Some(entry_index);
1563 }
1564
1565 Ok(())
1566}
1567
1568fn payload_entry_bytes<'a>(payload: &'a [u8], entry: &ResolvedPayloadEntry) -> Result<&'a [u8]> {
1569 let end = entry
1570 .byte_offset
1571 .checked_add(entry.byte_length)
1572 .context("payload entry range overflow")?;
1573
1574 payload
1575 .get(entry.byte_offset..end)
1576 .context("payload entry range exceeds reconstructed payload")
1577}
1578
1579fn report_gap_payload_body(out: &mut String, entry: &[u8]) -> Result<()> {
1580 let header = gap::validate_gap_payload(entry).context("invalid GAP payload")?;
1581 let filler_bytes = entry.len().saturating_sub(gap::GAP_HEADER_LENGTH);
1582
1583 writeln!(out, " GAP1 payload:")?;
1584 writeln!(out, " encoded bytes: {}", entry.len())?;
1585 writeln!(
1586 out,
1587 " declared payload bytes: {}",
1588 header.payload_byte_length
1589 )?;
1590 writeln!(out, " sample count: {}", header.sample_count)?;
1591 writeln!(out, " filler seed: 0x{:08x}", header.seed)?;
1592 writeln!(out, " filler bytes: {filler_bytes}")?;
1593
1594 Ok(())
1595}
1596
1597pub fn load_bundle_metadata(
1598 path: impl AsRef<std::path::Path>,
1599) -> Result<encodec_rs::metadata::OnnxFrameBundleMetadata> {
1600 let path = path.as_ref();
1601 let json = std::fs::read_to_string(path)
1602 .with_context(|| format!("failed to read bundle JSON {}", path.display()))?;
1603
1604 serde_json::from_str(&json).context("failed to deserialize OnnxFrameBundleMetadata")
1605}
1606
1607fn report_json_structure(out: &mut String, value: &serde_json::Value) -> Result<()> {
1608 match value {
1609 serde_json::Value::Object(object) => {
1610 writeln!(out, " JSON root: object")?;
1611 writeln!(out, " top-level fields: {}", object.len())?;
1612
1613 if !object.is_empty() {
1614 writeln!(out, " field names:")?;
1615 for (name, field_value) in object {
1616 writeln!(out, " {name}: {}", json_value_kind(field_value))?;
1617 }
1618 }
1619 }
1620 serde_json::Value::Array(array) => {
1621 writeln!(out, " JSON root: array")?;
1622 writeln!(out, " array items: {}", array.len())?;
1623
1624 if let Some(first) = array.first() {
1625 writeln!(out, " first item type: {}", json_value_kind(first))?;
1626 }
1627 }
1628 other => {
1629 writeln!(out, " JSON root: {}", json_value_kind(other))?;
1630 }
1631 }
1632
1633 Ok(())
1634}
1635
1636fn json_value_kind(value: &serde_json::Value) -> &'static str {
1637 match value {
1638 serde_json::Value::Null => "null",
1639 serde_json::Value::Bool(_) => "boolean",
1640 serde_json::Value::Number(_) => "number",
1641 serde_json::Value::String(_) => "string",
1642 serde_json::Value::Array(_) => "array",
1643 serde_json::Value::Object(_) => "object",
1644 }
1645}
1646
1647fn report_final_summary(
1648 out: &mut String,
1649 png: &[u8],
1650 descriptor: &RecordDescriptor,
1651 stream: &[u8],
1652 record_profile: &str,
1653) -> Result<()> {
1654 let parsed = parse_record_stream(stream)?;
1655 let checks = collect_spec_checks(&parsed, record_profile);
1656 let passed = checks.iter().filter(|check| check.passed).count();
1657 let failed = checks.len().saturating_sub(passed);
1658 let signed_reference_valid = descriptor
1659 .signed_release_reference
1660 .as_ref()
1661 .map(|reference| reference.validate())
1662 .transpose()
1663 .is_ok();
1664 let final_issuance_markers = descriptor.signed_release_reference.is_some()
1665 && descriptor.canonical_url.is_some()
1666 && descriptor.release_id.is_some();
1667 let overall_ok = failed == 0 && signed_reference_valid;
1668 let track_count = parsed.metadata.tracks.len();
1669 let payload_entries = parsed.metadata.payload_entries.len();
1670 let transport_chunks = parsed.chunks.len();
1671 let release_id = descriptor
1672 .release_id
1673 .map(record_descriptor::release_id_to_text)
1674 .unwrap_or_else(|| "absent".to_owned());
1675 let canonical_url = descriptor.canonical_url.as_deref().unwrap_or("absent");
1676 let catalogue_code = descriptor
1677 .canonical_url
1678 .as_deref()
1679 .and_then(yl_catalogue_code_from_url)
1680 .unwrap_or_else(|| "absent".to_owned());
1681 let signature_key = descriptor
1682 .signed_release_reference
1683 .as_ref()
1684 .and_then(|reference| printable_utf8(&reference.key_id))
1685 .unwrap_or("absent");
1686
1687 section(out, "SUMMARY");
1688
1689 writeln!(
1690 out,
1691 " {} {}",
1692 if overall_ok {
1693 green_tick()
1694 } else {
1695 red_cross()
1696 },
1697 if overall_ok {
1698 "VALID BITNEEDLE RECORD"
1699 } else {
1700 "RECORD HAS FAILURES"
1701 }
1702 )?;
1703 writeln!(
1704 out,
1705 " {} format checks: {passed}/{} passed",
1706 if failed == 0 {
1707 green_tick()
1708 } else {
1709 red_cross()
1710 },
1711 checks.len()
1712 )?;
1713 writeln!(
1714 out,
1715 " {} YL issuance: {}",
1716 if final_issuance_markers {
1717 green_tick()
1718 } else {
1719 red_cross()
1720 },
1721 if final_issuance_markers {
1722 "final markers present"
1723 } else {
1724 "incomplete or absent"
1725 }
1726 )?;
1727 writeln!(
1728 out,
1729 " {} signed reference: {}",
1730 if signed_reference_valid && descriptor.signed_release_reference.is_some() {
1731 green_tick()
1732 } else {
1733 red_cross()
1734 },
1735 match descriptor.signed_release_reference.as_ref() {
1736 Some(_) if signed_reference_valid =>
1737 "structurally valid; cryptographic verification not performed",
1738 Some(_) => "structurally invalid",
1739 None => "absent",
1740 }
1741 )?;
1742
1743 writeln!(out)?;
1744 writeln!(out, " RECORD")?;
1745 writeln!(
1746 out,
1747 " title: {}",
1748 descriptor.title.as_deref().unwrap_or("absent")
1749 )?;
1750 writeln!(
1751 out,
1752 " artist: {}",
1753 descriptor.artist.as_deref().unwrap_or("absent")
1754 )?;
1755 writeln!(out, " profile: {}", descriptor.record_profile)?;
1756 writeln!(
1757 out,
1758 " payload encoding: {}",
1759 descriptor.payload_encoding
1760 )?;
1761 writeln!(out, " PNG bytes: {}", png.len())?;
1762 writeln!(out, " BRS1 bytes: {}", stream.len())?;
1763 writeln!(out, " tracks: {track_count}")?;
1764 writeln!(out, " payload entries: {payload_entries}")?;
1765 writeln!(out, " transport chunks: {transport_chunks}")?;
1766
1767 writeln!(out)?;
1768 writeln!(out, " IDENTITY")?;
1769 writeln!(out, " release ID: {release_id}")?;
1770 writeln!(out, " YL catalogue code: {catalogue_code}")?;
1771 writeln!(out, " canonical URL: {canonical_url}")?;
1772 writeln!(
1773 out,
1774 " catalog number: {}",
1775 descriptor.catalog_number.as_deref().unwrap_or("absent")
1776 )?;
1777 writeln!(
1778 out,
1779 " created at (ms): {}",
1780 format_optional_number(descriptor.created_at)
1781 )?;
1782
1783 writeln!(out)?;
1784 writeln!(out, " SIGNATURE")?;
1785 writeln!(out, " key ID: {signature_key}")?;
1786 writeln!(
1787 out,
1788 " commitment: {}",
1789 descriptor
1790 .signed_release_reference
1791 .as_ref()
1792 .map(|reference| hex::encode(reference.release_commitment_sha256))
1793 .unwrap_or_else(|| "absent".to_owned())
1794 )?;
1795 writeln!(
1796 out,
1797 " signature bytes: {}",
1798 descriptor
1799 .signed_release_reference
1800 .as_ref()
1801 .map(|reference| reference.signature.len().to_string())
1802 .unwrap_or_else(|| "absent".to_owned())
1803 )?;
1804 writeln!(
1805 out,
1806 " BSC pointer: {}",
1807 descriptor
1808 .bsc_pointer
1809 .as_ref()
1810 .map(|pointer| format!("present ({} bytes)", pointer.len()))
1811 .unwrap_or_else(|| "absent".to_owned())
1812 )?;
1813
1814 if failed > 0 {
1815 writeln!(out)?;
1816 writeln!(
1817 out,
1818 " {} {failed} check(s) failed; see CHECK RESULTS above",
1819 red_cross()
1820 )?;
1821 }
1822
1823 Ok(())
1824}
1825
1826fn format_optional_text(value: Option<&str>) -> String {
1827 value
1828 .map(|text| format!("{text:?}"))
1829 .unwrap_or_else(|| "absent".to_owned())
1830}
1831
1832fn format_optional_number<T: std::fmt::Display>(value: Option<T>) -> String {
1833 value
1834 .map(|number| number.to_string())
1835 .unwrap_or_else(|| "absent".to_owned())
1836}
1837
1838fn yl_catalogue_code_from_url(url: &str) -> Option<String> {
1839 let slug = url.trim().trim_end_matches('/').rsplit('/').next()?.trim();
1840
1841 if slug.is_empty() {
1842 return None;
1843 }
1844
1845 let compact = slug
1846 .chars()
1847 .filter(|character| *character != '-')
1848 .collect::<String>()
1849 .to_ascii_uppercase();
1850
1851 if compact.len() != 11
1852 || !compact
1853 .chars()
1854 .all(|character| character.is_ascii_alphanumeric())
1855 {
1856 return None;
1857 }
1858
1859 Some(format!("yl_{compact}"))
1860}
1861
1862fn green_tick() -> &'static str {
1863 "\x1b[1;32m✓\x1b[0m"
1864}
1865
1866fn red_cross() -> &'static str {
1867 "\x1b[1;31m✗\x1b[0m"
1868}
1869
1870fn status_mark(passed: bool) -> &'static str {
1871 if passed {
1872 green_tick()
1873 } else {
1874 red_cross()
1875 }
1876}
1877
1878fn section(out: &mut String, title: &str) {
1879 let _ = writeln!(out, "\n=== {title} ===");
1880}
1881
1882fn indent(text: &str, spaces: usize) -> String {
1883 let pad = " ".repeat(spaces);
1884
1885 text.lines()
1886 .map(|line| format!("{pad}{line}"))
1887 .collect::<Vec<_>>()
1888 .join("\n")
1889}
1890
1891fn printable_utf8(bytes: &[u8]) -> Option<&str> {
1892 let text = std::str::from_utf8(bytes).ok()?;
1893
1894 if text
1895 .chars()
1896 .any(|character| character.is_control() && !matches!(character, '\n' | '\r' | '\t'))
1897 {
1898 return None;
1899 }
1900
1901 Some(text)
1902}
1903
1904fn ascii_magic(bytes: &[u8]) -> String {
1905 bytes
1906 .iter()
1907 .take(4)
1908 .map(|byte| {
1909 if (0x20..=0x7e).contains(byte) {
1910 *byte as char
1911 } else {
1912 '.'
1913 }
1914 })
1915 .collect()
1916}
1917
1918fn hex_prefix(bytes: &[u8], max: usize) -> String {
1919 let mut out = String::new();
1920 let clipped = bytes.len().min(max);
1921
1922 for offset in (0..clipped).step_by(16) {
1923 let end = (offset + 16).min(clipped);
1924 let chunk = &bytes[offset..end];
1925
1926 let hex = chunk
1927 .iter()
1928 .map(|byte| format!("{byte:02x}"))
1929 .collect::<Vec<_>>()
1930 .join(" ");
1931
1932 let ascii = chunk
1933 .iter()
1934 .map(|byte| {
1935 if (0x20..=0x7e).contains(byte) {
1936 *byte as char
1937 } else {
1938 '.'
1939 }
1940 })
1941 .collect::<String>();
1942
1943 out.push_str(&format!("{offset:08x}: {hex:<47} {ascii}\n"));
1944 }
1945
1946 if bytes.len() > max {
1947 out.push_str(&format!("... truncated {} bytes\n", bytes.len() - max));
1948 }
1949
1950 out
1951}
1952
1953fn png_ihdr(png: &[u8]) -> Option<(u32, u32, u8, u8)> {
1954 if png.len() < 33 || &png[..8] != b"\x89PNG\r\n\x1a\n" || &png[12..16] != b"IHDR" {
1955 return None;
1956 }
1957
1958 let width = u32::from_be_bytes(png[16..20].try_into().ok()?);
1959 let height = u32::from_be_bytes(png[20..24].try_into().ok()?);
1960
1961 Some((width, height, png[24], png[25]))
1962}
1963
1964#[allow(dead_code)]
1965fn container_code_name(code: u8) -> &'static str {
1966 match code {
1967 CONTAINER_ECDC => "ECDC",
1968 CONTAINER_MOSS_NANO => "MOSSNANO",
1969 CONTAINER_EXTENSION => "EXTENSION",
1970 _ => "UNKNOWN",
1971 }
1972}
1973
1974#[cfg(test)]
1975mod programme_summary_tests {
1976 use super::*;
1977 use record_core::{
1978 build_programme_map, Chunk, PayloadEntryDescriptor, RecordStream, TrackDescriptor,
1979 TrackGapDescriptor,
1980 };
1981
1982 fn ecdc_descriptor() -> record_core::PayloadDescriptor {
1983 record_core::ecdc::ecdc_payload_descriptor(
1984 48_000,
1985 2,
1986 &record_core::ecdc::EcdcCodecMetadata {
1987 model: "encodec_48khz".to_owned(),
1988 num_codebooks: 8,
1989 lm: true,
1990 fp_scale: 8192,
1991 min_range: 2,
1992 bitstream_version: 2,
1993 lm_frame_length: 203,
1994 },
1995 )
1996 .unwrap()
1997 }
1998
1999 fn three_tracks_two_gaps_record_stream() -> RecordStream {
2003 let entry = vec![0xABu8; 16];
2004 let entry_count = 60 + 2 + 60 + 2 + 61;
2005 let mut payload = Vec::with_capacity(entry_count * entry.len());
2006 let mut payload_entries = Vec::with_capacity(entry_count);
2007 for _ in 0..entry_count {
2008 payload.extend_from_slice(&entry);
2009 payload_entries.push(PayloadEntryDescriptor {
2010 byte_length: entry.len(),
2011 payload_descriptor_index: 0,
2012 });
2013 }
2014
2015 let metadata = RecordStreamMetadata {
2016 version: record_core::RECORD_STREAM_METADATA_VERSION,
2017 encrypted: false,
2018 payload_descriptors: vec![ecdc_descriptor()],
2019 payload_entries,
2020 tracks: vec![
2021 TrackDescriptor {
2022 title: "Track A".to_owned(),
2023 first_revolution_index: 0,
2024 revolution_count: 60,
2025 },
2026 TrackDescriptor {
2027 title: "Track B".to_owned(),
2028 first_revolution_index: 62,
2029 revolution_count: 60,
2030 },
2031 TrackDescriptor {
2032 title: "Track C".to_owned(),
2033 first_revolution_index: 124,
2034 revolution_count: 61,
2035 },
2036 ],
2037 track_gaps: vec![
2038 TrackGapDescriptor {
2039 first_revolution_index: 60,
2040 revolution_count: 2,
2041 after_track_index: 0,
2042 },
2043 TrackGapDescriptor {
2044 first_revolution_index: 122,
2045 revolution_count: 2,
2046 after_track_index: 1,
2047 },
2048 ],
2049 };
2050
2051 RecordStream {
2052 metadata,
2053 metadata_bytes: Vec::new(),
2054 chunks: vec![Chunk {
2055 payload,
2056 crc32: 0,
2057 nonce: None,
2058 }],
2059 }
2060 }
2061
2062 #[test]
2063 fn summary_reports_musical_and_track_gap_entries_separately() {
2064 let stream = three_tracks_two_gaps_record_stream();
2065 let mut out = String::new();
2066 report_stream_summary(&mut out, &stream).unwrap();
2067
2068 assert!(out.contains("tracks: 3"), "{out}");
2069 assert!(out.contains("TrackGap ranges: 2"), "{out}");
2070 assert!(out.contains("musical timeline entries: 181"), "{out}");
2071 assert!(out.contains("TrackGap timeline entries: 4"), "{out}");
2072 assert!(out.contains("total timeline entries: 185"), "{out}");
2073 }
2074
2075 #[test]
2076 fn record_passes_pre_decode_programme_map_validation() {
2077 let stream = three_tracks_two_gaps_record_stream();
2078 validate_track_listing_metadata(&stream.metadata).unwrap();
2079 let map = build_programme_map(&stream, Some("single45")).unwrap();
2080 assert_eq!(map.regions.len(), 7);
2083 assert_eq!(
2084 map.total_samples,
2085 185 * u64::from(record_core::ecdc::ECDC_OUTPUT_SAMPLES)
2086 );
2087 }
2088}