Skip to main content

test_spin/
lib.rs

1//! Reusable verbose inspection for Bitneedle PNG records.
2//!
3//! BRD1 and BRS1 remain compact binary wire formats. This library renders the
4//! decoded typed structures as labelled human-readable text for diagnostics.
5//! Canonical BRD1 and BRS1 data remains compact binary.
6
7use 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    /// Optional display name for the PNG.
21    pub png_name: Option<&'a str>,
22
23    /// Optional EnCodec bundle metadata used only for ECDC packet-layout
24    /// diagnostics.
25    pub bundle_metadata: Option<&'a encodec_rs::metadata::OnnxFrameBundleMetadata>,
26
27    /// Optional external release-manifest bytes. BRD1 contains only a binary
28    /// signed-release reference, so the complete manifest must be supplied
29    /// separately (or loaded from BSC/registry by a caller).
30    pub manifest: Option<ExternalManifest<'a>>,
31
32    /// Maximum number of transport chunks printed in detail.
33    pub max_chunks: usize,
34
35    /// Maximum prefix bytes printed for each binary object.
36    pub max_hex_bytes: usize,
37
38    /// Print every payload entry instead of the default first/last compact view.
39    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
61/// Decode and inspect a complete Bitneedle PNG.
62pub 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
114/// Render a decoded BRD1 descriptor.
115pub 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
276/// Render an external manifest without declaring any canonical manifest format.
277///
278/// If the bytes are UTF-8 JSON, they are pretty-printed solely for inspection.
279/// Otherwise printable UTF-8 or a hexadecimal prefix is shown. Hash comparison
280/// is deliberately left unresolved until algorithm-code semantics are fixed.
281pub 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
324/// Render a BRS1 stream and its logical payload entries.
325pub 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    // Track-gap entries are reported purely from the explicit `track_gaps`
394    // metadata — never by sniffing a payload container, classifying small
395    // ECDC entries as gaps, or inferring gaps from entries left uncovered by
396    // a track. A gap's bytes are an ordinary payload entry (ECDC in
397    // practice); only this metadata says what it is.
398    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    /// Three musical tracks (60 + 60 + 61 = 181 entries) separated by two
2000    /// explicit two-revolution track gaps (4 entries): 185 entries total, no
2001    /// GAP container or GAP1 payload anywhere — every entry is ordinary ECDC.
2002    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        // 3 track regions + 4 gap regions (gap regions are not merged across
2081        // consecutive entries, unlike same-track regions): 7 total.
2082        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}